Merge "Deprecate `toPublisher(lifecycle, liveData)`" into androidx-main
diff --git a/.github/CODEOWNERS b/.github/CODEOWNERS
index a760252..733504c 100644
--- a/.github/CODEOWNERS
+++ b/.github/CODEOWNERS
@@ -4,7 +4,7 @@
 *          @dlam @yigit
 
 # Owners for each library group:
-/activity*          @sanura-njaka @jbw0033 @ianhanniballake
+/activity*          @jbw0033 @ianhanniballake
 /appcompat/*        @alanv
 /biometric/*        @jbolinger
 /collection/*       @dlam
@@ -12,7 +12,7 @@
 /compose/runtime/*  @jimgoog @lelandrichardson
 /core/*             @alanv
 /datastore/*        @rohitsat13 @yigit
-/fragment/*         @sanura-njaka @jbw0033 @ianhanniballake
+/fragment/*         @jbw0033 @ianhanniballake
 /lifecycle/*        @jbw0033 @ianhanniballake
 /navigation/*       @jbw0033 @ianhanniballake @claraf3
 /paging/*           @claraf3 @ianhanniballake
diff --git a/annotation/annotation-experimental-lint/build.gradle b/annotation/annotation-experimental-lint/build.gradle
index 4deb784..d10e6d1 100644
--- a/annotation/annotation-experimental-lint/build.gradle
+++ b/annotation/annotation-experimental-lint/build.gradle
@@ -48,7 +48,6 @@
 androidx {
     name = "Experimental annotation lint checks"
     type = LibraryType.LINT
-    mavenVersion = LibraryVersions.ANNOTATION_EXPERIMENTAL
     inceptionYear = "2019"
     description = "Lint checks for the Experimental annotation library. Also enforces the " +
             "semantics of Kotlin @Experimental APIs from within Android Java source code."
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 22093b6..719a480 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
@@ -73,7 +73,6 @@
 import org.jetbrains.uast.UastFacade
 import org.jetbrains.uast.getContainingUClass
 import org.jetbrains.uast.getContainingUMethod
-import org.jetbrains.uast.java.JavaUAnnotation
 import org.jetbrains.uast.toUElement
 import org.jetbrains.uast.toUElementOfType
 import org.jetbrains.uast.tryResolve
@@ -376,7 +375,7 @@
             }
 
             if (relevantAnnotations.contains(signature)) {
-                val uAnnotation = JavaUAnnotation.wrap(annotation)
+                val uAnnotation = annotation.toUElementOfType<UAnnotation>() ?: continue
 
                 // Common case: there's just one annotation; no need to create a list copy
                 if (length == 1) {
@@ -888,7 +887,7 @@
 ): Boolean = optInFqNames.any { optInFqName ->
     annotations.any { annotation ->
         annotation.hasQualifiedName(optInFqName) &&
-            ((annotation.toUElement() as? UAnnotation)?.hasMatchingAttributeValueClass(
+            ((annotation.toUElementOfType<UAnnotation>())?.hasMatchingAttributeValueClass(
                 "markerClass",
                 annotationFqName,
             ) ?: false)
diff --git a/annotation/annotation-replacewith-lint/integration-tests/build.gradle b/annotation/annotation-replacewith-lint/integration-tests/build.gradle
deleted file mode 100644
index 4211c69..0000000
--- a/annotation/annotation-replacewith-lint/integration-tests/build.gradle
+++ /dev/null
@@ -1,41 +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.
- */
-
-import androidx.build.LibraryType
-import org.jetbrains.kotlin.gradle.tasks.KotlinCompile
-
-def isIdeBuild() {
-    return project.properties['android.injected.invoked.from.ide'] == 'true'
-}
-
-plugins {
-    id("AndroidXPlugin")
-    id("com.android.library")
-    id("kotlin-android")
-}
-
-android {
-    namespace "androidx.annotation.replacewith.lint.integrationtests"
-}
-
-dependencies {
-    implementation(libs.kotlinStdlib)
-    implementation(project(":annotation:annotation-replacewith"))
-}
-
-androidx {
-    type = LibraryType.INTERNAL_TEST_LIBRARY
-}
diff --git a/annotation/annotation-replacewith-lint/src/main/resources/META-INF/services/com.android.tools.lint.client.api.IssueRegistry b/annotation/annotation-replacewith-lint/src/main/resources/META-INF/services/com.android.tools.lint.client.api.IssueRegistry
deleted file mode 100644
index 9ebd6a0..0000000
--- a/annotation/annotation-replacewith-lint/src/main/resources/META-INF/services/com.android.tools.lint.client.api.IssueRegistry
+++ /dev/null
@@ -1 +0,0 @@
-androidx.annotation.replacewith.lint.ReplaceWithIssueRegistry
diff --git a/annotation/annotation-replacewith-lint/src/test/kotlin/androidx/annotation/replacewith/lint/ApiLintVersionsTest.kt b/annotation/annotation-replacewith-lint/src/test/kotlin/androidx/annotation/replacewith/lint/ApiLintVersionsTest.kt
deleted file mode 100644
index 938b868..0000000
--- a/annotation/annotation-replacewith-lint/src/test/kotlin/androidx/annotation/replacewith/lint/ApiLintVersionsTest.kt
+++ /dev/null
@@ -1,39 +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.annotation.replacewith.lint
-
-import com.android.tools.lint.client.api.LintClient
-import com.android.tools.lint.detector.api.CURRENT_API
-import org.junit.Assert.assertEquals
-import org.junit.Test
-import org.junit.runner.RunWith
-import org.junit.runners.JUnit4
-
-@RunWith(JUnit4::class)
-class ApiLintVersionsTest {
-
-    @Test
-    fun versionsCheck() {
-        LintClient.clientName = LintClient.CLIENT_UNIT_TESTS
-
-        val registry = ReplaceWithIssueRegistry()
-        // We hardcode version registry.api to the version that is used to run tests.
-        assertEquals("registry.api matches version used to run tests", CURRENT_API, registry.api)
-        // Intentionally fails in IDE, because we use different API version in Studio and CLI.
-        assertEquals("registry.minApi is set to minimum level of 10", 10, registry.minApi)
-    }
-}
diff --git a/annotation/annotation-replacewith/api/current.txt b/annotation/annotation-replacewith/api/current.txt
deleted file mode 100644
index 1147234..0000000
--- a/annotation/annotation-replacewith/api/current.txt
+++ /dev/null
@@ -1,9 +0,0 @@
-// Signature format: 4.0
-package androidx.annotation {
-
-  @java.lang.annotation.Retention(java.lang.annotation.RetentionPolicy.CLASS) @java.lang.annotation.Target({java.lang.annotation.ElementType.METHOD, java.lang.annotation.ElementType.FIELD, java.lang.annotation.ElementType.CONSTRUCTOR}) public @interface ReplaceWith {
-    method public abstract String expression();
-  }
-
-}
-
diff --git a/annotation/annotation-replacewith/api/res-current.txt b/annotation/annotation-replacewith/api/res-current.txt
deleted file mode 100644
index e69de29..0000000
--- a/annotation/annotation-replacewith/api/res-current.txt
+++ /dev/null
diff --git a/annotation/annotation-replacewith/api/restricted_current.txt b/annotation/annotation-replacewith/api/restricted_current.txt
deleted file mode 100644
index 1147234..0000000
--- a/annotation/annotation-replacewith/api/restricted_current.txt
+++ /dev/null
@@ -1,9 +0,0 @@
-// Signature format: 4.0
-package androidx.annotation {
-
-  @java.lang.annotation.Retention(java.lang.annotation.RetentionPolicy.CLASS) @java.lang.annotation.Target({java.lang.annotation.ElementType.METHOD, java.lang.annotation.ElementType.FIELD, java.lang.annotation.ElementType.CONSTRUCTOR}) public @interface ReplaceWith {
-    method public abstract String expression();
-  }
-
-}
-
diff --git a/annotation/annotation-replacewith/build.gradle b/annotation/annotation-replacewith/build.gradle
deleted file mode 100644
index 97cd77b..0000000
--- a/annotation/annotation-replacewith/build.gradle
+++ /dev/null
@@ -1,46 +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.
- */
-
-
-import androidx.build.KotlinTarget
-import androidx.build.Publish
-
-plugins {
-    id("AndroidXPlugin")
-    id("com.android.library")
-    id("org.jetbrains.kotlin.android")
-}
-
-dependencies {
-    api(libs.kotlinStdlib)
-    lintPublish(project(":annotation:annotation-replacewith-lint"))
-}
-
-androidx {
-    name = "ReplaceWith annotation"
-    publish = Publish.SNAPSHOT_AND_RELEASE
-    mavenVersion = LibraryVersions.ANNOTATION_REPLACEWITH
-    kotlinTarget = KotlinTarget.KOTLIN_1_7
-    inceptionYear = "2019"
-    description = "Java annotation for use on deprecated API surfaces with a replacement. When " +
-            "used in conjunction with the ReplaceWith annotation lint checks, this annotation " +
-            "provides functional parity with Kotlin's Deprecated annotation ReplaceWith feature."
-    metalavaK2UastEnabled = true
-}
-
-android {
-    namespace "androidx.annotation.replacewith"
-}
diff --git a/annotation/annotation/api/current.txt b/annotation/annotation/api/current.txt
index 71556c1..b5d93db 100644
--- a/annotation/annotation/api/current.txt
+++ b/annotation/annotation/api/current.txt
@@ -241,6 +241,13 @@
   @kotlin.annotation.MustBeDocumented @kotlin.annotation.Retention(kotlin.annotation.AnnotationRetention.BINARY) @kotlin.annotation.Target(allowedTargets={kotlin.annotation.AnnotationTarget.FUNCTION, kotlin.annotation.AnnotationTarget.PROPERTY_GETTER, kotlin.annotation.AnnotationTarget.PROPERTY_SETTER, kotlin.annotation.AnnotationTarget.VALUE_PARAMETER, kotlin.annotation.AnnotationTarget.FIELD, kotlin.annotation.AnnotationTarget.LOCAL_VARIABLE}) public @interface RawRes {
   }
 
+  @kotlin.annotation.Target(allowedTargets={kotlin.annotation.AnnotationTarget.FUNCTION, kotlin.annotation.AnnotationTarget.FIELD, kotlin.annotation.AnnotationTarget.CONSTRUCTOR}) public @interface ReplaceWith {
+    method public abstract String expression();
+    method public abstract String[] imports();
+    property public abstract String expression;
+    property public abstract String[] imports;
+  }
+
   @java.lang.annotation.Target({java.lang.annotation.ElementType.TYPE, java.lang.annotation.ElementType.METHOD, java.lang.annotation.ElementType.CONSTRUCTOR, java.lang.annotation.ElementType.FIELD, java.lang.annotation.ElementType.PACKAGE}) @kotlin.annotation.MustBeDocumented @kotlin.annotation.Retention(kotlin.annotation.AnnotationRetention.BINARY) @kotlin.annotation.Target(allowedTargets={kotlin.annotation.AnnotationTarget.ANNOTATION_CLASS, 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.FILE}) public @interface RequiresApi {
     method public abstract int api() default 1;
     method public abstract int value() default 1;
diff --git a/annotation/annotation/api/restricted_current.txt b/annotation/annotation/api/restricted_current.txt
index 71556c1..b5d93db 100644
--- a/annotation/annotation/api/restricted_current.txt
+++ b/annotation/annotation/api/restricted_current.txt
@@ -241,6 +241,13 @@
   @kotlin.annotation.MustBeDocumented @kotlin.annotation.Retention(kotlin.annotation.AnnotationRetention.BINARY) @kotlin.annotation.Target(allowedTargets={kotlin.annotation.AnnotationTarget.FUNCTION, kotlin.annotation.AnnotationTarget.PROPERTY_GETTER, kotlin.annotation.AnnotationTarget.PROPERTY_SETTER, kotlin.annotation.AnnotationTarget.VALUE_PARAMETER, kotlin.annotation.AnnotationTarget.FIELD, kotlin.annotation.AnnotationTarget.LOCAL_VARIABLE}) public @interface RawRes {
   }
 
+  @kotlin.annotation.Target(allowedTargets={kotlin.annotation.AnnotationTarget.FUNCTION, kotlin.annotation.AnnotationTarget.FIELD, kotlin.annotation.AnnotationTarget.CONSTRUCTOR}) public @interface ReplaceWith {
+    method public abstract String expression();
+    method public abstract String[] imports();
+    property public abstract String expression;
+    property public abstract String[] imports;
+  }
+
   @java.lang.annotation.Target({java.lang.annotation.ElementType.TYPE, java.lang.annotation.ElementType.METHOD, java.lang.annotation.ElementType.CONSTRUCTOR, java.lang.annotation.ElementType.FIELD, java.lang.annotation.ElementType.PACKAGE}) @kotlin.annotation.MustBeDocumented @kotlin.annotation.Retention(kotlin.annotation.AnnotationRetention.BINARY) @kotlin.annotation.Target(allowedTargets={kotlin.annotation.AnnotationTarget.ANNOTATION_CLASS, 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.FILE}) public @interface RequiresApi {
     method public abstract int api() default 1;
     method public abstract int value() default 1;
diff --git a/annotation/annotation/src/commonMain/kotlin/androidx/annotation/RestrictTo.kt b/annotation/annotation/src/commonMain/kotlin/androidx/annotation/RestrictTo.kt
index 1c29d6a..c988b44 100644
--- a/annotation/annotation/src/commonMain/kotlin/androidx/annotation/RestrictTo.kt
+++ b/annotation/annotation/src/commonMain/kotlin/androidx/annotation/RestrictTo.kt
@@ -91,7 +91,7 @@
         @Deprecated(
             message = "Use @RestrictTo(LIBRARY_GROUP_PREFIX) instead",
             replaceWith =
-                ReplaceWith(
+                kotlin.ReplaceWith(
                     "LIBRARY_GROUP_PREFIX",
                     "androidx.annotation.RestrictTo.Scope.LIBRARY_GROUP_PREFIX"
                 )
diff --git a/annotation/annotation-replacewith/src/main/java/androidx/annotation/ReplaceWith.java b/annotation/annotation/src/jvmMain/kotlin/androidx/annotation/ReplaceWith.jvm.kt
similarity index 62%
rename from annotation/annotation-replacewith/src/main/java/androidx/annotation/ReplaceWith.java
rename to annotation/annotation/src/jvmMain/kotlin/androidx/annotation/ReplaceWith.jvm.kt
index a9c70b9..6b45187 100644
--- a/annotation/annotation-replacewith/src/main/java/androidx/annotation/ReplaceWith.java
+++ b/annotation/annotation/src/jvmMain/kotlin/androidx/annotation/ReplaceWith.jvm.kt
@@ -13,22 +13,15 @@
  * See the License for the specific language governing permissions and
  * limitations under the License.
  */
-
-package androidx.annotation;
-
-import static java.lang.annotation.ElementType.CONSTRUCTOR;
-import static java.lang.annotation.ElementType.FIELD;
-import static java.lang.annotation.ElementType.METHOD;
-
-import java.lang.annotation.Target;
+package androidx.annotation
 
 /**
- * Specifies a code fragment that can be used to suggest a replacement for a method in
- * conjunction with the `ReplaceWith` lint check.
- * <p>
- * The {@code expression} parameter specified the replacement expression, which is interpreted in
- * the context of the symbol being used and can reference members of the enclosing classes, etc.
- * <p>
+ * Specifies a code fragment that can be used to suggest a replacement for a method in conjunction
+ * with the `ReplaceWith` lint check.
+ *
+ * The `expression` parameter specified the replacement expression, which is interpreted in the
+ * context of the symbol being used and can reference members of the enclosing classes, etc.
+ *
  * For method calls, the replacement expression may contain parameter names of the method being
  * replaced, which will be substituted with actual arguments used in the call being replaced:
  * <pre>
@@ -36,7 +29,9 @@
  * static int getActionType(AccessibilityEvent event, int slot) { ... }
  * </pre>
  */
-@Target({METHOD, FIELD, CONSTRUCTOR})
-public @interface ReplaceWith {
-    String expression();
-}
+@Target(
+    AnnotationTarget.FUNCTION,
+    AnnotationTarget.FIELD,
+    AnnotationTarget.CONSTRUCTOR
+)
+public annotation class ReplaceWith(val expression: String, vararg val imports: String = [])
diff --git a/appcompat/appcompat/src/androidTest/java/androidx/appcompat/app/BaseBasicsTestCase.java b/appcompat/appcompat/src/androidTest/java/androidx/appcompat/app/BaseBasicsTestCase.java
index 8e12de3..9cf5964 100644
--- a/appcompat/appcompat/src/androidTest/java/androidx/appcompat/app/BaseBasicsTestCase.java
+++ b/appcompat/appcompat/src/androidTest/java/androidx/appcompat/app/BaseBasicsTestCase.java
@@ -222,7 +222,7 @@
     }
 
     @Test
-    @SdkSuppress(minSdkVersion = 28)
+    @SdkSuppress(minSdkVersion = 28, maxSdkVersion = 33) // maxSdk 33 b/322355781
     @RequiresApi(28)
     public void testOnApplyWindowInsetsReachesContent_withDisplayCutout() throws Throwable {
         final A activity = mActivityTestRule.getActivity();
diff --git a/appcompat/appcompat/src/androidTest/java/androidx/appcompat/app/BasicsTestCaseWithWindowDecor.java b/appcompat/appcompat/src/androidTest/java/androidx/appcompat/app/BasicsTestCaseWithWindowDecor.java
index e5364f8..3e8b934 100644
--- a/appcompat/appcompat/src/androidTest/java/androidx/appcompat/app/BasicsTestCaseWithWindowDecor.java
+++ b/appcompat/appcompat/src/androidTest/java/androidx/appcompat/app/BasicsTestCaseWithWindowDecor.java
@@ -18,6 +18,7 @@
 
 import androidx.test.annotation.UiThreadTest;
 import androidx.test.filters.LargeTest;
+import androidx.test.filters.SdkSuppress;
 
 import org.junit.Test;
 
@@ -27,6 +28,7 @@
         super(WindowDecorAppCompatActivity.class);
     }
 
+    @SdkSuppress(maxSdkVersion = 33) // b/322355781
     @Test
     @UiThreadTest
     public void testSupportActionModeAppCompatCallbacks() {
diff --git a/appcompat/appcompat/src/androidTest/java/androidx/appcompat/app/DrawerLayoutTest.java b/appcompat/appcompat/src/androidTest/java/androidx/appcompat/app/DrawerLayoutTest.java
index 1b9b9b1..4ce752e 100755
--- a/appcompat/appcompat/src/androidTest/java/androidx/appcompat/app/DrawerLayoutTest.java
+++ b/appcompat/appcompat/src/androidTest/java/androidx/appcompat/app/DrawerLayoutTest.java
@@ -56,6 +56,7 @@
 import androidx.test.filters.FlakyTest;
 import androidx.test.filters.LargeTest;
 import androidx.test.filters.MediumTest;
+import androidx.test.filters.SdkSuppress;
 import androidx.test.filters.Suppress;
 import androidx.test.rule.ActivityTestRule;
 
@@ -484,6 +485,7 @@
         mDrawerLayout.removeDrawerListener(mockedListener);
     }
 
+    @SdkSuppress(maxSdkVersion = 33) // b/321813959
     @Test
     @LargeTest
     public void testDrawerListenerCallbacksOnClosingViaSwipes() {
diff --git a/benchmark/baseline-profile-gradle-plugin/src/main/kotlin/androidx/baselineprofile/gradle/utils/AgpPlugin.kt b/benchmark/baseline-profile-gradle-plugin/src/main/kotlin/androidx/baselineprofile/gradle/utils/AgpPlugin.kt
index aab26d0..ea70bdf 100644
--- a/benchmark/baseline-profile-gradle-plugin/src/main/kotlin/androidx/baselineprofile/gradle/utils/AgpPlugin.kt
+++ b/benchmark/baseline-profile-gradle-plugin/src/main/kotlin/androidx/baselineprofile/gradle/utils/AgpPlugin.kt
@@ -161,8 +161,8 @@
         }
 
         var applied = false
-        variants.all {
-            if (applied) return@all
+        variants.configureEach {
+            if (applied) return@configureEach
             applied = true
 
             // Execute all the scheduled variant blocks
diff --git a/benchmark/baseline-profile-gradle-plugin/src/main/kotlin/androidx/baselineprofile/gradle/utils/Constants.kt b/benchmark/baseline-profile-gradle-plugin/src/main/kotlin/androidx/baselineprofile/gradle/utils/Constants.kt
index 5dfd830..ebe0a317 100644
--- a/benchmark/baseline-profile-gradle-plugin/src/main/kotlin/androidx/baselineprofile/gradle/utils/Constants.kt
+++ b/benchmark/baseline-profile-gradle-plugin/src/main/kotlin/androidx/baselineprofile/gradle/utils/Constants.kt
@@ -20,7 +20,7 @@
 
 // Minimum AGP version required
 internal val MIN_AGP_VERSION_REQUIRED = AndroidPluginVersion(8, 0, 0)
-internal val MAX_AGP_VERSION_REQUIRED = AndroidPluginVersion(8, 3, 0)
+internal val MAX_AGP_VERSION_REQUIRED = AndroidPluginVersion(8, 4, 0)
 
 // Prefix for the build type baseline profile
 internal const val BUILD_TYPE_BASELINE_PROFILE_PREFIX = "nonMinified"
diff --git a/benchmark/benchmark-common/src/main/cpp/CMakeLists.txt b/benchmark/benchmark-common/src/main/cpp/CMakeLists.txt
index f61a216..e9b01a2 100644
--- a/benchmark/benchmark-common/src/main/cpp/CMakeLists.txt
+++ b/benchmark/benchmark-common/src/main/cpp/CMakeLists.txt
@@ -35,3 +35,4 @@
 target_link_libraries(${CMAKE_PROJECT_NAME} PUBLIC log)
 target_link_libraries(${CMAKE_PROJECT_NAME} PRIVATE dl)
 target_link_libraries(${CMAKE_PROJECT_NAME} PUBLIC android)
+target_link_options(${CMAKE_PROJECT_NAME} PRIVATE "-Wl,-z,max-page-size=16384")
diff --git a/benchmark/benchmark-darwin-gradle-plugin/src/main/kotlin/androidx/benchmark/darwin/gradle/DarwinBenchmarkPlugin.kt b/benchmark/benchmark-darwin-gradle-plugin/src/main/kotlin/androidx/benchmark/darwin/gradle/DarwinBenchmarkPlugin.kt
index a6ad725..05bf753 100644
--- a/benchmark/benchmark-darwin-gradle-plugin/src/main/kotlin/androidx/benchmark/darwin/gradle/DarwinBenchmarkPlugin.kt
+++ b/benchmark/benchmark-darwin-gradle-plugin/src/main/kotlin/androidx/benchmark/darwin/gradle/DarwinBenchmarkPlugin.kt
@@ -40,7 +40,7 @@
         project.plugins.withType(KotlinMultiplatformPluginWrapper::class.java) {
             val multiplatformExtension: KotlinMultiplatformExtension =
                 project.extensions.getByType(it.projectExtensionClass.java)
-            multiplatformExtension.targets.all { kotlinTarget ->
+            multiplatformExtension.targets.configureEach { kotlinTarget ->
                 if (kotlinTarget is KotlinNativeTarget) {
                     if (kotlinTarget.konanTarget.family.isAppleFamily) {
                         // We want to apply the plugin only once.
diff --git a/benchmark/benchmark-macro/api/current.txt b/benchmark/benchmark-macro/api/current.txt
index 3800229..0bb3341 100644
--- a/benchmark/benchmark-macro/api/current.txt
+++ b/benchmark/benchmark-macro/api/current.txt
@@ -45,6 +45,10 @@
   @SuppressCompatibility @kotlin.RequiresOptIn(message="This Metric API is experimental.") @kotlin.annotation.Retention(kotlin.annotation.AnnotationRetention.BINARY) @kotlin.annotation.Target(allowedTargets={kotlin.annotation.AnnotationTarget.CLASS, kotlin.annotation.AnnotationTarget.FUNCTION}) public @interface ExperimentalMetricApi {
   }
 
+  @SuppressCompatibility @androidx.benchmark.macro.ExperimentalMetricApi public final class FrameTimingGfxInfoMetric extends androidx.benchmark.macro.Metric {
+    ctor public FrameTimingGfxInfoMetric();
+  }
+
   public final class FrameTimingMetric extends androidx.benchmark.macro.Metric {
     ctor public FrameTimingMetric();
   }
diff --git a/benchmark/benchmark-macro/api/restricted_current.txt b/benchmark/benchmark-macro/api/restricted_current.txt
index f96634cf..b7caaa5 100644
--- a/benchmark/benchmark-macro/api/restricted_current.txt
+++ b/benchmark/benchmark-macro/api/restricted_current.txt
@@ -58,6 +58,10 @@
   @SuppressCompatibility @kotlin.RequiresOptIn(message="This Metric API is experimental.") @kotlin.annotation.Retention(kotlin.annotation.AnnotationRetention.BINARY) @kotlin.annotation.Target(allowedTargets={kotlin.annotation.AnnotationTarget.CLASS, kotlin.annotation.AnnotationTarget.FUNCTION}) public @interface ExperimentalMetricApi {
   }
 
+  @SuppressCompatibility @androidx.benchmark.macro.ExperimentalMetricApi public final class FrameTimingGfxInfoMetric extends androidx.benchmark.macro.Metric {
+    ctor public FrameTimingGfxInfoMetric();
+  }
+
   public final class FrameTimingMetric extends androidx.benchmark.macro.Metric {
     ctor public FrameTimingMetric();
   }
diff --git a/benchmark/benchmark-macro/src/androidTest/java/androidx/benchmark/macro/perfetto/FrameTimingQueryTest.kt b/benchmark/benchmark-macro/src/androidTest/java/androidx/benchmark/macro/perfetto/FrameTimingQueryTest.kt
index be313c4..e56cea9 100644
--- a/benchmark/benchmark-macro/src/androidTest/java/androidx/benchmark/macro/perfetto/FrameTimingQueryTest.kt
+++ b/benchmark/benchmark-macro/src/androidTest/java/androidx/benchmark/macro/perfetto/FrameTimingQueryTest.kt
@@ -18,6 +18,7 @@
 
 import androidx.benchmark.macro.createTempFileFromAsset
 import androidx.benchmark.macro.perfetto.FrameTimingQuery.SubMetric.FrameDurationCpuNs
+import androidx.benchmark.macro.perfetto.FrameTimingQuery.SubMetric.FrameDurationFullNs
 import androidx.benchmark.macro.perfetto.FrameTimingQuery.SubMetric.FrameDurationUiNs
 import androidx.benchmark.macro.perfetto.FrameTimingQuery.SubMetric.FrameOverrunNs
 import androidx.benchmark.macro.perfetto.FrameTimingQuery.getFrameSubMetrics
@@ -26,6 +27,7 @@
 import androidx.test.ext.junit.runners.AndroidJUnit4
 import androidx.test.filters.MediumTest
 import kotlin.test.assertEquals
+import kotlin.test.assertTrue
 import org.junit.Assume.assumeTrue
 import org.junit.Test
 import org.junit.runner.RunWith
@@ -82,16 +84,17 @@
 
         assertEquals(
             expected = mapOf(
-                FrameDurationCpuNs to listOf(6881407L, 5648542L, 3830261L, 4343438L),
                 FrameDurationUiNs to listOf(2965052L, 3246407L, 1562188L, 1945469L),
-                FrameOverrunNs to listOf(-5207137L, -11699862L, -14025295L, -12300155L)
+                FrameDurationCpuNs to listOf(6881407L, 5648542L, 3830261L, 4343438L),
+                FrameDurationFullNs to listOf(15292863L, 8800138L, 6474705L, 8199845L),
+                FrameOverrunNs to listOf(-5207137L, -11699862L, -14025295L, -12300155L),
             ),
             actual = frameSubMetrics.mapValues {
                 it.value.subList(0, 4)
             }
         )
         assertEquals(
-            expected = List(3) { 96 },
+            expected = List(FrameTimingQuery.SubMetric.values().size) { 96 },
             actual = frameSubMetrics.map { it.value.size },
             message = "Expect same number of frames for each metric"
         )
@@ -142,15 +145,45 @@
 
         assertEquals(
             // Note: it's correct for UI to be > CPU in cases below,
-            // since UI is be sleeping after RT is done
+            // since UI is sleeping after RT is done
             expected = mapOf(
-                FrameDurationCpuNs to listOf(7304479L, 7567188L, 8064897L, 8434115L),
                 FrameDurationUiNs to listOf(4253646L, 7592761L, 8088855L, 8461876L),
-                FrameOverrunNs to listOf(-9009770L, -12199949L, -11299378L, -11708522L)
+                FrameDurationCpuNs to listOf(7304479L, 7567188L, 8064897L, 8434115L),
+                FrameDurationFullNs to listOf(11490230L, 8300051L, 9200622L, 8791478L),
+                FrameOverrunNs to listOf(-9009770L, -12199949L, -11299378L, -11708522L),
             ),
             actual = frameData.getFrameSubMetrics(captureApiLevel = 33).mapValues {
                 it.value.subList(0, 4)
             }
         )
     }
+
+    @MediumTest
+    @Test
+    fun fixedTrace34_invalidExpectActual() {
+        assumeTrue(isAbiSupported())
+        val traceFile =
+            createTempFileFromAsset("api34_invalid_expect_actual", ".perfetto-trace")
+
+        val frameData = PerfettoTraceProcessor.runSingleSessionServer(
+            traceFile.absolutePath
+        ) {
+            FrameTimingQuery.getFrameData(
+                session = this,
+                captureApiLevel = 34,
+                packageName = "androidx.compose.integration.macrobenchmark.target"
+            )
+        }
+
+        assertTrue(
+            frameData
+                .getFrameSubMetrics(34)[FrameOverrunNs]!!
+                .all { it < 50_000_000 }
+        )
+        assertTrue(
+            frameData.none {
+                it.actualSlice!!.frameId == 110752 || it.expectedSlice!!.frameId == 110752
+            }
+        )
+    }
 }
diff --git a/benchmark/benchmark-macro/src/main/java/androidx/benchmark/macro/JankCollectionHelper.java b/benchmark/benchmark-macro/src/main/java/androidx/benchmark/macro/JankCollectionHelper.java
new file mode 100644
index 0000000..d0b11e8
--- /dev/null
+++ b/benchmark/benchmark-macro/src/main/java/androidx/benchmark/macro/JankCollectionHelper.java
@@ -0,0 +1,378 @@
+/*
+ * 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.benchmark.macro;
+
+import android.text.TextUtils;
+import android.util.Log;
+
+import androidx.annotation.NonNull;
+import androidx.annotation.Nullable;
+import androidx.annotation.VisibleForTesting;
+import androidx.test.platform.app.InstrumentationRegistry;
+import androidx.test.uiautomator.UiDevice;
+
+import java.io.IOException;
+import java.util.Collections;
+import java.util.HashMap;
+import java.util.HashSet;
+import java.util.Map;
+import java.util.Set;
+import java.util.regex.Matcher;
+import java.util.regex.Pattern;
+
+/** Collects jank metrics for all or a list of processes. */
+class JankCollectionHelper {
+
+    private static final String LOG_TAG = JankCollectionHelper.class.getSimpleName();
+
+    // Prefix for all output metrics that come from the gfxinfo dump.
+    @VisibleForTesting static final String GFXINFO_METRICS_PREFIX = "gfxinfo";
+    // Shell dump commands to get and reset the tracked gfxinfo metrics.
+    @VisibleForTesting static final String GFXINFO_COMMAND_GET = "dumpsys gfxinfo %s";
+    @VisibleForTesting static final String GFXINFO_COMMAND_RESET = GFXINFO_COMMAND_GET + " reset";
+    // Pattern matchers and enumerators to verify and pull gfxinfo metrics.
+    // Example: "** Graphics info for pid 853 [com.google.android.leanbacklauncher] **"
+    private static final String GFXINFO_OUTPUT_HEADER = "Graphics info for pid (\\d+) \\[(%s)\\]";
+    // Note: use the [\\s\\S]* multi-line matcher to support String#matches(). Instead of splitting
+    // the larger sections into more granular lines, we can match across all lines for simplicity.
+    private static final String MULTILINE_MATCHER = "[\\s\\S]*%s[\\s\\S]*";
+
+    public enum GfxInfoMetric {
+        // Example: "Total frames rendered: 20391"
+        TOTAL_FRAMES(
+                Pattern.compile(".*Total frames rendered: (\\d+).*", Pattern.DOTALL),
+                1,
+                "total_frames"),
+        // Example: "Janky frames: 785 (3.85%)"
+        JANKY_FRAMES_COUNT(
+                Pattern.compile(
+                        ".*Janky frames: (\\d+) \\(([0-9]+[\\.]?[0-9]+)\\%\\).*", Pattern.DOTALL),
+                1,
+                "janky_frames_count"),
+        // Example: "Janky frames: 785 (3.85%)"
+        JANKY_FRAMES_PRCNT(
+                Pattern.compile(
+                        ".*Janky frames: (\\d+) \\(([0-9]+[\\.]?[0-9]+)\\%\\).*", Pattern.DOTALL),
+                2,
+                "janky_frames_percent"),
+        // Example: "Janky frames (legacy): 785 (3.85%)"
+        JANKY_FRAMES_LEGACY_COUNT(
+                Pattern.compile(
+                        ".*Janky frames \\(legacy\\): (\\d+) \\(([0-9]+[\\.]?[0-9]+)\\%\\).*",
+                        Pattern.DOTALL),
+                1,
+                "janky_frames_legacy_count"),
+        // Example: "Janky frames (legacy): 785 (3.85%)"
+        JANKY_FRAMES_LEGACY_PRCNT(
+                Pattern.compile(
+                        ".*Janky frames \\(legacy\\): (\\d+) \\(([0-9]+[\\.]?[0-9]+)\\%\\).*",
+                        Pattern.DOTALL),
+                2,
+                "janky_frames_legacy_percent"),
+        // Example: "50th percentile: 9ms"
+        FRAME_TIME_50TH(
+                Pattern.compile(".*50th percentile: (\\d+)ms.*", Pattern.DOTALL),
+                1,
+                "frame_render_time_percentile_50"),
+        // Example: "90th percentile: 9ms"
+        FRAME_TIME_90TH(
+                Pattern.compile(".*90th percentile: (\\d+)ms.*", Pattern.DOTALL),
+                1,
+                "frame_render_time_percentile_90"),
+        // Example: "95th percentile: 9ms"
+        FRAME_TIME_95TH(
+                Pattern.compile(".*95th percentile: (\\d+)ms.*", Pattern.DOTALL),
+                1,
+                "frame_render_time_percentile_95"),
+        // Example: "99th percentile: 9ms"
+        FRAME_TIME_99TH(
+                Pattern.compile(".*99th percentile: (\\d+)ms.*", Pattern.DOTALL),
+                1,
+                "frame_render_time_percentile_99"),
+        // Example: "Number Missed Vsync: 0"
+        NUM_MISSED_VSYNC(
+                Pattern.compile(".*Number Missed Vsync: (\\d+).*", Pattern.DOTALL),
+                1,
+                "missed_vsync"),
+        // Example: "Number High input latency: 0"
+        NUM_HIGH_INPUT_LATENCY(
+                Pattern.compile(".*Number High input latency: (\\d+).*", Pattern.DOTALL),
+                1,
+                "high_input_latency"),
+        // Example: "Number Slow UI thread: 0"
+        NUM_SLOW_UI_THREAD(
+                Pattern.compile(".*Number Slow UI thread: (\\d+).*", Pattern.DOTALL),
+                1,
+                "slow_ui_thread"),
+        // Example: "Number Slow bitmap uploads: 0"
+        NUM_SLOW_BITMAP_UPLOADS(
+                Pattern.compile(".*Number Slow bitmap uploads: (\\d+).*", Pattern.DOTALL),
+                1,
+                "slow_bmp_upload"),
+        // Example: "Number Slow issue draw commands: 0"
+        NUM_SLOW_DRAW(
+                Pattern.compile(".*Number Slow issue draw commands: (\\d+).*", Pattern.DOTALL),
+                1,
+                "slow_issue_draw_cmds"),
+        // Example: "Number Frame deadline missed: 0"
+        NUM_FRAME_DEADLINE_MISSED(
+                Pattern.compile(".*Number Frame deadline missed: (\\d+).*", Pattern.DOTALL),
+                1,
+                "deadline_missed"),
+        // Number Frame deadline missed (legacy): 0
+        NUM_FRAME_DEADLINE_MISSED_LEGACY(
+                Pattern.compile(
+                        ".*Number Frame deadline missed \\(legacy\\): (\\d+).*", Pattern.DOTALL),
+                1,
+                "deadline_missed_legacy"),
+        // Example: "50th gpu percentile: 9ms"
+        GPU_FRAME_TIME_50TH(
+                Pattern.compile(".*50th gpu percentile: (\\d+)ms.*", Pattern.DOTALL),
+                1,
+                "gpu_frame_render_time_percentile_50"),
+        // Example: "90th gpu percentile: 9ms"
+        GPU_FRAME_TIME_90TH(
+                Pattern.compile(".*90th gpu percentile: (\\d+)ms.*", Pattern.DOTALL),
+                1,
+                "gpu_frame_render_time_percentile_90"),
+        // Example: "95th gpu percentile: 9ms"
+        GPU_FRAME_TIME_95TH(
+                Pattern.compile(".*95th gpu percentile: (\\d+)ms.*", Pattern.DOTALL),
+                1,
+                "gpu_frame_render_time_percentile_95"),
+        // Example: "99th gpu percentile: 9ms"
+        GPU_FRAME_TIME_99TH(
+                Pattern.compile(".*99th gpu percentile: (\\d+)ms.*", Pattern.DOTALL),
+                1,
+                "gpu_frame_render_time_percentile_99");
+
+        private final Pattern mPattern;
+        private final int mGroupIndex;
+        private final String mMetricId;
+
+        GfxInfoMetric(Pattern pattern, int groupIndex, String metricId) {
+            mPattern = pattern;
+            mGroupIndex = groupIndex;
+            mMetricId = metricId;
+        }
+
+        @Nullable
+        public Double parse(@NonNull String lines) {
+            Matcher matcher = mPattern.matcher(lines);
+            if (matcher.matches()) {
+                return Double.valueOf(matcher.group(mGroupIndex));
+            } else {
+                return null;
+            }
+        }
+
+        @NonNull
+        public String getMetricId() {
+            return mMetricId;
+        }
+    }
+
+    private final Set<String> mTrackedPackages = new HashSet<>();
+    private UiDevice mDevice;
+
+    /** Clear existing jank metrics, unless explicitly configured. */
+    public boolean startCollecting() {
+        if (mTrackedPackages.isEmpty()) {
+            clearGfxInfo();
+        } else {
+            int exceptionCount = 0;
+            Exception lastException = null;
+            for (String pkg : mTrackedPackages) {
+                try {
+                    clearGfxInfo(pkg);
+                } catch (Exception e) {
+                    Log.e(LOG_TAG, "Encountered exception resetting gfxinfo.", e);
+                    lastException = e;
+                    exceptionCount++;
+                }
+            }
+            // Throw exceptions after to not quit on a single failure.
+            if (exceptionCount > 1) {
+                throw new RuntimeException(
+                        "Multiple exceptions were encountered resetting gfxinfo. Reporting the last"
+                                + " one only; others are visible in logs.",
+                        lastException);
+            } else if (exceptionCount == 1) {
+                throw new RuntimeException(
+                        "Encountered exception resetting gfxinfo.", lastException);
+            }
+        }
+        // No exceptions denotes success.
+        return true;
+    }
+
+    /** Collect the {@code gfxinfo} metrics for tracked processes (or all, if unspecified). */
+    @NonNull
+    public Map<String, Double> getMetrics() {
+        Map<String, Double> result = new HashMap<>();
+        if (mTrackedPackages.isEmpty()) {
+            result.putAll(getGfxInfoMetrics());
+        } else {
+            int exceptionCount = 0;
+            Exception lastException = null;
+            for (String pkg : mTrackedPackages) {
+                try {
+                    result.putAll(getGfxInfoMetrics(pkg));
+                } catch (Exception e) {
+                    Log.e(LOG_TAG, "Encountered exception getting gfxinfo.", e);
+                    lastException = e;
+                    exceptionCount++;
+                }
+            }
+            // Throw exceptions after to ensure all failures are reported. The metrics will still
+            // not be collected at this point, but it will possibly make the issue cause clearer.
+            if (exceptionCount > 1) {
+                throw new RuntimeException(
+                        "Multiple exceptions were encountered getting gfxinfo. Reporting the last"
+                                + " one only; others are visible in logs.",
+                        lastException);
+            } else if (exceptionCount == 1) {
+                throw new RuntimeException("Encountered exception getting gfxinfo.", lastException);
+            }
+        }
+        return result;
+    }
+
+    /** Do nothing, because nothing is needed to disable jank. */
+    public boolean stopCollecting() {
+        return true;
+    }
+
+    /** Add a package or list of packages to be tracked. */
+    public void addTrackedPackages(@NonNull String... packages) {
+        Collections.addAll(mTrackedPackages, packages);
+    }
+
+    /** Clear the {@code gfxinfo} for all packages. */
+    @VisibleForTesting
+    void clearGfxInfo() {
+        // Not specifying a package will clear everything.
+        clearGfxInfo("");
+    }
+
+    /** Clear the {@code gfxinfo} for the {@code pkg} specified. */
+    @VisibleForTesting
+    void clearGfxInfo(String pkg) {
+        try {
+            if (pkg.isEmpty()) {
+                String command = String.format(GFXINFO_COMMAND_RESET, "--");
+                String output = getDevice().executeShellCommand(command);
+                // Success if any header (set by passing an empty-string) exists in the output.
+                verifyMatches(output, getHeaderMatcher(""), "No package headers in output.");
+                Log.v(LOG_TAG, "Cleared all gfxinfo.");
+            } else {
+                String command = String.format(GFXINFO_COMMAND_RESET, pkg);
+                String output = getDevice().executeShellCommand(command);
+                // Success if the specified package header exists in the output.
+                verifyMatches(output, getHeaderMatcher(pkg), "No package header in output.");
+                Log.v(LOG_TAG, String.format("Cleared %s gfxinfo.", pkg));
+            }
+        } catch (IOException e) {
+            throw new RuntimeException("Failed to clear gfxinfo.", e);
+        }
+    }
+
+    /** Return a {@code Map<String, Double>} of {@code gfxinfo} metrics for all processes. */
+    @VisibleForTesting
+    Map<String, Double> getGfxInfoMetrics() {
+        return getGfxInfoMetrics("");
+    }
+
+    /** Return a {@code Map<String, Double>} of {@code gfxinfo} metrics for {@code pkg}. */
+    @VisibleForTesting
+    @SuppressWarnings("StringSplitter")
+    Map<String, Double> getGfxInfoMetrics(String pkg) {
+        try {
+            String command = String.format(GFXINFO_COMMAND_GET, pkg);
+            String output = getDevice().executeShellCommand(command);
+            verifyMatches(output, getHeaderMatcher(pkg), "Missing package header.");
+            // Split each new section starting with two asterisks '**', and then query and append
+            // all metrics. This method supports both single-package and multi-package outputs.
+            String[] pkgMetricSections = output.split("\n\\*\\*");
+            Map<String, Double> result = new HashMap<>();
+            // Skip the 1st section, which contains only header information.
+            for (int i = 1; i < pkgMetricSections.length; i++) {
+                result.putAll(parseGfxInfoMetrics(pkgMetricSections[i]));
+            }
+            return result;
+        } catch (IOException e) {
+            throw new RuntimeException("Failed to get gfxinfo.", e);
+        }
+    }
+
+    /** Parse the {@code output} of {@code gfxinfo} to a {@code Map<String, Double>} of metrics. */
+    private Map<String, Double> parseGfxInfoMetrics(String output) {
+        Matcher header = Pattern.compile(getHeaderMatcher("")).matcher(output);
+        if (!header.matches()) {
+            throw new RuntimeException("Failed to parse package from gfxinfo output.");
+        }
+        // Package name is the only required field.
+        String packageName = header.group(2);
+        Log.v(LOG_TAG, String.format("Collecting metrics for: %s", packageName));
+        // Parse each metric from the results via a common pattern.
+        Map<String, Double> results = new HashMap<String, Double>();
+        for (GfxInfoMetric metric : GfxInfoMetric.values()) {
+            String metricKey =
+                    constructKey(GFXINFO_METRICS_PREFIX, packageName, metric.getMetricId());
+            // Find the metric or log that it's missing.
+            Double value = metric.parse(output);
+            if (value == null) {
+                Log.d(LOG_TAG, String.format("Did not find %s from %s", metricKey, packageName));
+            } else {
+                results.put(metricKey, value);
+            }
+        }
+        return results;
+    }
+
+    private String constructKey(@NonNull String ...tokens) {
+        return TextUtils.join("_", tokens);
+    }
+
+    /**
+     * Returns a matcher {@code String} for {@code pkg}'s {@code gfxinfo} headers.
+     *
+     * <p>Note: {@code pkg} may be empty.
+     */
+    private String getHeaderMatcher(String pkg) {
+        return String.format(
+                MULTILINE_MATCHER,
+                String.format(GFXINFO_OUTPUT_HEADER, (pkg.isEmpty() ? ".*" : pkg)));
+    }
+
+    /** Verify the {@code output} matches {@code match}, or throw if not. */
+    private void verifyMatches(String output, String match, String message, Object... args) {
+        if (!output.matches(match)) {
+            throw new IllegalStateException(String.format(message, args));
+        }
+    }
+
+    /** Returns the {@link UiDevice} under test. */
+    @NonNull
+    @VisibleForTesting
+    protected UiDevice getDevice() {
+        if (mDevice == null) {
+            mDevice = UiDevice.getInstance(InstrumentationRegistry.getInstrumentation());
+        }
+        return mDevice;
+    }
+}
diff --git a/benchmark/benchmark-macro/src/main/java/androidx/benchmark/macro/Metric.kt b/benchmark/benchmark-macro/src/main/java/androidx/benchmark/macro/Metric.kt
index 7423f68..260e340 100644
--- a/benchmark/benchmark-macro/src/main/java/androidx/benchmark/macro/Metric.kt
+++ b/benchmark/benchmark-macro/src/main/java/androidx/benchmark/macro/Metric.kt
@@ -16,13 +16,13 @@
 
 package androidx.benchmark.macro
 
-import android.os.Build
 import androidx.annotation.RequiresApi
 import androidx.annotation.RestrictTo
 import androidx.benchmark.Shell
 import androidx.benchmark.macro.BatteryCharge.hasMinimumCharge
 import androidx.benchmark.macro.PowerMetric.Type
 import androidx.benchmark.macro.PowerRail.hasMetrics
+import androidx.benchmark.macro.TraceSectionMetric.Mode
 import androidx.benchmark.macro.perfetto.BatteryDischargeQuery
 import androidx.benchmark.macro.perfetto.FrameTimingQuery
 import androidx.benchmark.macro.perfetto.FrameTimingQuery.SubMetric
@@ -151,10 +151,10 @@
     ): List<Measurement> {
         return FrameTimingQuery.getFrameData(
             session = traceSession,
-            captureApiLevel = Build.VERSION.SDK_INT,
+            captureApiLevel = captureInfo.apiLevel,
             packageName = captureInfo.targetPackageName
         )
-            .getFrameSubMetrics(Build.VERSION.SDK_INT)
+            .getFrameSubMetrics(captureInfo.apiLevel)
             .filterKeys { it == SubMetric.FrameDurationCpuNs || it == SubMetric.FrameOverrunNs }
             .map {
                 Measurement(
@@ -170,6 +170,116 @@
 }
 
 /**
+ * Version of FrameTimingMetric based on 'dumpsys gfxinfo' instead of trace data.
+ *
+ * Added for experimentation in contrast to FrameTimingMetric, as the platform accounting of frame
+ * drops currently behaves differently from that of FrameTimingMetric.
+ *
+ * Likely to be removed when differences in jank behavior are reconciled between this class, and
+ * [FrameTimingMetric].
+ *
+ * Note that output metrics do not match perfectly to FrameTimingMetric, as individual frame times
+ * are not available, only high level, millisecond-precision statistics.
+ */
+@ExperimentalMetricApi
+class FrameTimingGfxInfoMetric : Metric() {
+    private lateinit var packageName: String
+    private val helper = JankCollectionHelper()
+    private var metrics = mutableMapOf<String, Double>()
+
+    override fun configure(packageName: String) {
+        this.packageName = packageName
+        helper.addTrackedPackages(packageName)
+    }
+
+    override fun start() {
+        try {
+            helper.startCollecting()
+        } catch (exception: RuntimeException) {
+            // Ignore the exception that might result from trying to clear GfxInfo
+            // The current implementation of JankCollectionHelper throws a RuntimeException
+            // when that happens. This is safe to ignore because the app being benchmarked
+            // is not showing any UI when this happens typically.
+
+            // Once the MacroBenchmarkRule has the ability to setup the app in the right state via
+            // a designated setup block, we can get rid of this.
+            if (!Shell.isPackageAlive(packageName)) {
+                error(exception.message ?: "Assertion error, $packageName not running")
+            }
+        }
+    }
+
+    override fun stop() {
+        helper.stopCollecting()
+
+        // save metrics on stop to attempt to more closely match perfetto based metrics
+        metrics.clear()
+        metrics.putAll(helper.metrics)
+    }
+
+    /**
+     * Used to convert keys from platform to JSON format.
+     *
+     * This both converts `snake_case_format` to `camelCaseFormat`, and renames for clarity.
+     *
+     * Note that these will still output to inst results in snake_case, with `MetricNameUtils`
+     * via [androidx.benchmark.MetricResult.putInBundle].
+     */
+    private val keyRenameMap = mapOf(
+        "frame_render_time_percentile_50" to "gfxFrameTime50thPercentileMs",
+        "frame_render_time_percentile_90" to "gfxFrameTime90thPercentileMs",
+        "frame_render_time_percentile_95" to "gfxFrameTime95thPercentileMs",
+        "frame_render_time_percentile_99" to "gfxFrameTime99thPercentileMs",
+        "gpu_frame_render_time_percentile_50" to "gpuFrameTime50thPercentileMs",
+        "gpu_frame_render_time_percentile_90" to "gpuFrameTime90thPercentileMs",
+        "gpu_frame_render_time_percentile_95" to "gpuFrameTime95thPercentileMs",
+        "gpu_frame_render_time_percentile_99" to "gpuFrameTime99thPercentileMs",
+        "missed_vsync" to "vsyncMissedFrameCount",
+        "deadline_missed" to "deadlineMissedFrameCount",
+        "deadline_missed_legacy" to "deadlineMissedFrameCountLegacy",
+        "janky_frames_count" to "jankyFrameCount",
+        "janky_frames_legacy_count" to "jankyFrameCountLegacy",
+        "high_input_latency" to "highInputLatencyFrameCount",
+        "slow_ui_thread" to "slowUiThreadFrameCount",
+        "slow_bmp_upload" to "slowBitmapUploadFrameCount",
+        "slow_issue_draw_cmds" to "slowIssueDrawCommandsFrameCount",
+        "total_frames" to "gfxFrameTotalCount",
+        "janky_frames_percent" to "gfxFrameJankPercent",
+        "janky_frames_legacy_percent" to "jankyFramePercentLegacy"
+    )
+
+    /**
+     * Filters output to only frameTimeXXthPercentileMs and totalFrameCount
+     */
+    private val keyAllowList = setOf(
+        "gfxFrameTime50thPercentileMs",
+        "gfxFrameTime90thPercentileMs",
+        "gfxFrameTime95thPercentileMs",
+        "gfxFrameTime99thPercentileMs",
+        "gfxFrameTotalCount",
+        "gfxFrameJankPercent",
+    )
+
+    override fun getResult(
+        captureInfo: CaptureInfo,
+        traceSession: PerfettoTraceProcessor.Session
+    ): List<Measurement> {
+        return metrics
+            .map {
+                val prefix = "gfxinfo_${packageName}_"
+                val keyWithoutPrefix = it.key.removePrefix(prefix)
+
+                if (keyWithoutPrefix != it.key && keyRenameMap.containsKey(keyWithoutPrefix)) {
+                    Measurement(keyRenameMap[keyWithoutPrefix]!!, it.value)
+                } else {
+                    throw IllegalStateException("Unexpected key ${it.key}")
+                }
+            }
+            .filter { keyAllowList.contains(it.name) }
+    }
+}
+
+/**
  * Captures app startup timing metrics.
  *
  * This outputs the following measurements:
diff --git a/benchmark/benchmark-macro/src/main/java/androidx/benchmark/macro/perfetto/FrameTimingQuery.kt b/benchmark/benchmark-macro/src/main/java/androidx/benchmark/macro/perfetto/FrameTimingQuery.kt
index 7cec6f9..77deebb 100644
--- a/benchmark/benchmark-macro/src/main/java/androidx/benchmark/macro/perfetto/FrameTimingQuery.kt
+++ b/benchmark/benchmark-macro/src/main/java/androidx/benchmark/macro/perfetto/FrameTimingQuery.kt
@@ -63,12 +63,17 @@
     """.trimIndent()
 
     enum class SubMetric {
+        // Duration of UI thread
         FrameDurationCpuNs,
+        // Total duration from UI through RT slice
         FrameDurationUiNs,
-        FrameOverrunNs;
+        // How much longer did frame take than expected
+        FrameOverrunNs,
+        // Total duration from expected frame start through true end of frame
+        FrameDurationFullNs;
 
         fun supportedOnApiLevel(apiLevel: Int): Boolean {
-            return apiLevel >= 31 || this != FrameOverrunNs
+            return apiLevel >= 31 || this != FrameOverrunNs && this != FrameDurationFullNs
         }
     }
 
@@ -98,6 +103,10 @@
                     // workaround b/279088460, where actual slice ends too early
                     maxOf(actualSlice!!.endTs, rtSlice.endTs) - expectedSlice!!.endTs
                 }
+                SubMetric.FrameDurationFullNs -> {
+                    // workaround b/279088460, where actual slice ends too early
+                    maxOf(actualSlice!!.endTs, rtSlice.endTs) - expectedSlice!!.ts
+                }
             }
         }
         companion object {
@@ -209,13 +218,19 @@
                 //     the complete end of frame is present, and we want to discard those. This
                 //     doesn't happen at front of trace, since we find actuals from the end.
                 if (uiSlice != null) {
-                    // Use fixed offset since synthetic tracepoint for actual may start after the
-                    // actual UI slice (have observed 2us in practice)
-                    val actualSlice = actualSlicesPool.lastOrNull { it.ts < uiSlice.ts + 50_000 }
+                    val actualSlice = actualSlicesPool.lastOrNull {
+                        // Use fixed offset since synthetic tracepoint for actual may start after the
+                        // actual UI slice (have observed 2us in practice)
+                        it.ts < uiSlice.ts + 50_000 &&
+                            // ensure there's some overlap - if actual doesn't contain ui, may just
+                            // be "abandoned" slice at beginning of trace
+                            it.contains(uiSlice.ts + (uiSlice.dur / 2))
+                    }
                     actualSlicesPool.remove(actualSlice)
                     val expectedSlice = actualSlice?.frameId?.run {
                         expectedSlices.binarySearchFrameId(this)
                     }
+
                     FrameData.tryCreate31(
                         uiSlice = uiSlice,
                         rtSlice = rtSlice,
diff --git a/benchmark/gradle-plugin/src/main/kotlin/androidx/benchmark/gradle/BenchmarkPlugin.kt b/benchmark/gradle-plugin/src/main/kotlin/androidx/benchmark/gradle/BenchmarkPlugin.kt
index 5661083..b2e0efe 100644
--- a/benchmark/gradle-plugin/src/main/kotlin/androidx/benchmark/gradle/BenchmarkPlugin.kt
+++ b/benchmark/gradle-plugin/src/main/kotlin/androidx/benchmark/gradle/BenchmarkPlugin.kt
@@ -133,10 +133,10 @@
             )
         }
 
-        // NOTE: .all here is a Gradle API, which will run the callback passed to it after the
-        // extension variants have been resolved.
+        // NOTE: .configureEach here is a Gradle API, which will run the callback passed to it after
+        // the extension variants have been resolved.
         var applied = false
-        extensionVariants.all {
+        extensionVariants.configureEach {
             if (!applied) {
                 applied = true
 
diff --git a/buildSrc-tests/src/test/java/androidx/build/clang/KonanBuildServiceTest.kt b/buildSrc-tests/src/test/java/androidx/build/clang/KonanBuildServiceTest.kt
index 2209002b..77e83fd 100644
--- a/buildSrc-tests/src/test/java/androidx/build/clang/KonanBuildServiceTest.kt
+++ b/buildSrc-tests/src/test/java/androidx/build/clang/KonanBuildServiceTest.kt
@@ -22,6 +22,7 @@
 import java.io.File
 import org.gradle.api.GradleException
 import org.gradle.api.file.DirectoryProperty
+import org.jetbrains.kotlin.konan.target.Family
 import org.jetbrains.kotlin.konan.target.KonanTarget
 import org.junit.Before
 import org.junit.Test
@@ -114,6 +115,26 @@
         assertThat(strings).contains("Hello, World!")
         // should link with libc
         assertThat(strings).contains("libc")
+
+        // verify shared lib files are aligned to 16Kb boundary for Android targets
+        if (sharedLibraryParameters.konanTarget.get().asKonanTarget.family == Family.ANDROID) {
+            val alignment = ProcessBuilder("objdump", "-p", outputFile.path)
+                .start()
+                .inputStream
+                .bufferedReader()
+                .useLines { lines ->
+                    lines.filter {
+                        it.contains("LOAD")
+                    }.map {
+                        it.split(" ").last()
+                    }.firstOrNull()
+                }
+            assertThat(
+                alignment
+            ).isEqualTo(
+                "2**14"
+            )
+        }
     }
 
     @Test
diff --git a/buildSrc/private/src/main/kotlin/androidx/build/AndroidXComposeImplPlugin.kt b/buildSrc/private/src/main/kotlin/androidx/build/AndroidXComposeImplPlugin.kt
index bec1d87..06824dc 100644
--- a/buildSrc/private/src/main/kotlin/androidx/build/AndroidXComposeImplPlugin.kt
+++ b/buildSrc/private/src/main/kotlin/androidx/build/AndroidXComposeImplPlugin.kt
@@ -29,23 +29,16 @@
 import org.gradle.api.artifacts.type.ArtifactTypeDefinition
 import org.gradle.api.attributes.Attribute
 import org.gradle.api.plugins.ExtraPropertiesExtension
-import org.gradle.api.tasks.ClasspathNormalizer
 import org.gradle.api.tasks.bundling.Zip
 import org.gradle.kotlin.dsl.create
+import org.jetbrains.kotlin.gradle.plugin.CompilerPluginConfig
 import org.jetbrains.kotlin.gradle.plugin.KotlinBasePluginWrapper
 import org.jetbrains.kotlin.gradle.plugin.KotlinMultiplatformPluginWrapper
+import org.jetbrains.kotlin.gradle.plugin.SubpluginOption
 import org.jetbrains.kotlin.gradle.tasks.KotlinCompile
 
-const val composeSourceOption =
-    "plugin:androidx.compose.compiler.plugins.kotlin:sourceInformation=true"
-const val composeMetricsOption =
-    "plugin:androidx.compose.compiler.plugins.kotlin:metricsDestination"
-const val composeReportsOption =
-    "plugin:androidx.compose.compiler.plugins.kotlin:reportsDestination"
 const val zipComposeReportsTaskName = "zipComposeCompilerReports"
 const val zipComposeMetricsTaskName = "zipComposeCompilerMetrics"
-const val composeStrongSkippingOption =
-    "plugin:androidx.compose.compiler.plugins.kotlin:experimentalStrongSkipping"
 
 /** Plugin to apply common configuration for Compose projects. */
 class AndroidXComposeImplPlugin : Plugin<Project> {
@@ -232,7 +225,7 @@
             "androidx.compose.compiler:compiler:$versionToUse"
         )
 
-        val kotlinPlugin =
+        val kotlinPluginProvider = project.provider {
             configuration.incoming
                 .artifactView { view ->
                     view.attributes { attributes ->
@@ -243,6 +236,7 @@
                     }
                 }
                 .files
+        }
 
         val enableMetrics = project.enableComposeCompilerMetrics()
         val enableReports = project.enableComposeCompilerReports()
@@ -250,64 +244,55 @@
         val compileTasks = project.tasks.withType(KotlinCompile::class.java)
 
         compileTasks.configureEach { compile ->
-            // Append inputs to KotlinCompile so tasks get invalidated if any of these values change
-            compile.inputs
-                .files({ kotlinPlugin })
-                .withPropertyName("composeCompilerExtension")
-                .withNormalizer(ClasspathNormalizer::class.java)
             compile.inputs.property("composeMetricsEnabled", enableMetrics)
             compile.inputs.property("composeReportsEnabled", enableReports)
 
-            // Gradle hack ahead, we use of absolute paths, but is OK here because we do it in
-            // doFirst which happens after Gradle task input snapshotting. AGP does the same.
-            compile.doFirst {
-                compile.kotlinOptions.freeCompilerArgs += "-Xplugin=${kotlinPlugin.first()}"
+            compile.pluginClasspath.from(kotlinPluginProvider.get())
+            compile.addPluginOption(ComposeCompileOptions.StrongSkippingOption, "true")
 
-                // Enable Compose strong skipping mode
-                compile.kotlinOptions.freeCompilerArgs +=
-                    listOf("-P", "$composeStrongSkippingOption=true")
-
-                if (shouldPublish) {
-                    compile.kotlinOptions.freeCompilerArgs += listOf("-P", composeSourceOption)
-                }
+            if (shouldPublish) {
+                compile.addPluginOption(ComposeCompileOptions.SourceOption, "true")
             }
         }
 
         if (enableMetrics) {
-            project.rootProject.tasks.named(zipComposeMetricsTaskName).configure({ zipTask ->
+            project.rootProject.tasks.named(zipComposeMetricsTaskName).configure { zipTask ->
                 zipTask.dependsOn(compileTasks)
-            })
+            }
 
             val metricsIntermediateDir = project.compilerMetricsIntermediatesDir()
             compileTasks.configureEach { compile ->
-                compile.doFirst {
-                    compile.kotlinOptions.freeCompilerArgs +=
-                        listOf(
-                            "-P",
-                            "$composeMetricsOption=$metricsIntermediateDir"
-                        )
-                }
+                compile.addPluginOption(
+                    ComposeCompileOptions.MetricsOption, metricsIntermediateDir.path)
             }
         }
         if (enableReports) {
-            project.rootProject.tasks.named(zipComposeReportsTaskName).configure({ zipTask ->
+            project.rootProject.tasks.named(zipComposeReportsTaskName).configure { zipTask ->
                 zipTask.dependsOn(compileTasks)
-            })
+            }
 
             val reportsIntermediateDir = project.compilerReportsIntermediatesDir()
             compileTasks.configureEach { compile ->
-                compile.doFirst {
-                    compile.kotlinOptions.freeCompilerArgs +=
-                        listOf(
-                            "-P",
-                            "$composeReportsOption=$reportsIntermediateDir"
-                        )
-                }
+                compile.addPluginOption(
+                    ComposeCompileOptions.ReportsOption,
+                    reportsIntermediateDir.path
+                )
             }
         }
     }
 }
 
+private fun KotlinCompile.addPluginOption(
+    composeCompileOptions: ComposeCompileOptions,
+    value: String
+) =
+    pluginOptions.add(CompilerPluginConfig().apply {
+                addPluginArgument(
+                    composeCompileOptions.pluginId,
+                    SubpluginOption(composeCompileOptions.key, value))
+    }
+)
+
 public fun Project.zipComposeCompilerMetrics() {
     if (project.enableComposeCompilerMetrics()) {
         val zipComposeMetrics = project.tasks.register(zipComposeMetricsTaskName, Zip::class.java) {
@@ -347,3 +332,10 @@
 fun Project.composeCompilerDataDir(): File {
     return File(getDistributionDirectory(), "compose-compiler-data")
 }
+
+private enum class ComposeCompileOptions(val pluginId: String, val key: String) {
+    SourceOption("androidx.compose.compiler.plugins.kotlin", "sourceInformation"),
+    MetricsOption("androidx.compose.compiler.plugins.kotlin", "metricsDestination"),
+    ReportsOption("androidx.compose.compiler.plugins.kotlin", "reportsDestination"),
+    StrongSkippingOption("androidx.compose.compiler.plugins.kotlin", "experimentalStrongSkipping");
+}
diff --git a/buildSrc/private/src/main/kotlin/androidx/build/AndroidXGradleProperties.kt b/buildSrc/private/src/main/kotlin/androidx/build/AndroidXGradleProperties.kt
index 9735b71..a10d912 100644
--- a/buildSrc/private/src/main/kotlin/androidx/build/AndroidXGradleProperties.kt
+++ b/buildSrc/private/src/main/kotlin/androidx/build/AndroidXGradleProperties.kt
@@ -123,6 +123,11 @@
 const val HIGH_MEMORY = "androidx.highMemory"
 
 /**
+ * Negates the HIGH_MEMORY flag
+ */
+const val LOW_MEMORY = "androidx.lowMemory"
+
+/**
  * If true, don't require lint-checks project to exist. This should only be set in integration
  * tests, to allow them to save time by not configuring extra projects.
  */
@@ -163,6 +168,7 @@
         DISPLAY_TEST_OUTPUT,
         ENABLE_DOCUMENTATION,
         HIGH_MEMORY,
+        LOW_MEMORY,
         STUDIO_TYPE,
         SUMMARIZE_STANDARD_ERROR,
         USE_MAX_DEP_VERSIONS,
diff --git a/buildSrc/private/src/main/kotlin/androidx/build/AndroidXImplPlugin.kt b/buildSrc/private/src/main/kotlin/androidx/build/AndroidXImplPlugin.kt
index 9a59c99..010d52d 100644
--- a/buildSrc/private/src/main/kotlin/androidx/build/AndroidXImplPlugin.kt
+++ b/buildSrc/private/src/main/kotlin/androidx/build/AndroidXImplPlugin.kt
@@ -927,6 +927,11 @@
         @Suppress("DEPRECATION")
         externalNativeBuild.cmake.buildStagingDirectory =
             File(project.buildDir, "../nativeBuildStaging")
+
+        // Align the ELF region of native shared libs 16kb boundary
+        defaultConfig.externalNativeBuild.cmake.arguments.add(
+            "-DCMAKE_SHARED_LINKER_FLAGS=-Wl,-z,max-page-size=16384"
+        )
     }
 
     private fun KotlinMultiplatformAndroidTarget.configureAndroidBaseOptions(
diff --git a/buildSrc/private/src/main/kotlin/androidx/build/LintConfiguration.kt b/buildSrc/private/src/main/kotlin/androidx/build/LintConfiguration.kt
index 5a4ad81..6ed3943 100644
--- a/buildSrc/private/src/main/kotlin/androidx/build/LintConfiguration.kt
+++ b/buildSrc/private/src/main/kotlin/androidx/build/LintConfiguration.kt
@@ -228,19 +228,27 @@
     project.tasks.withType<LintModelWriterTask>().configureEach { it.variantInputs.addSourceSets() }
 }
 
+private fun Project.findLintProject(path: String): Project? {
+    return project.rootProject.findProject(path)
+        ?: if (allowMissingLintProject()) {
+            null
+        } else {
+            throw GradleException("Project $path does not exist")
+        }
+}
+
 private fun Project.configureLint(lint: Lint, isLibrary: Boolean) {
     val extension = project.androidXExtension
     val isMultiplatform = project.multiplatformExtension != null
-    val lintChecksProject =
-        project.rootProject.findProject(":lint-checks")
-            ?: if (allowMissingLintProject()) {
-                return
-            } else {
-                throw GradleException("Project :lint-checks does not exist")
-            }
-
+    val lintChecksProject = findLintProject(":lint-checks") ?: return
     project.dependencies.add("lintChecks", lintChecksProject)
 
+    if (extension.type == LibraryType.GRADLE_PLUGIN) {
+        project.rootProject.findProject(":lint:lint-gradle")?.let {
+            project.dependencies.add("lintChecks", it)
+        }
+    }
+
     afterEvaluate { addSourceSetsForAndroidMultiplatformAfterEvaluate() }
 
     // The purpose of this specific project is to test that lint is running, so
@@ -359,6 +367,9 @@
             disable.add("IllegalExperimentalApiUsage")
         }
 
+        fatal.add("UastImplementation") // go/hide-uast-impl
+        fatal.add("KotlincFE10") // b/239982263
+
         // If the project has not overridden the lint config, set the default one.
         if (lintConfig == null) {
             val lintXmlPath =
diff --git a/buildSrc/private/src/main/kotlin/androidx/build/clang/KonanBuildService.kt b/buildSrc/private/src/main/kotlin/androidx/build/clang/KonanBuildService.kt
index 5d3c3aa..6e74707 100644
--- a/buildSrc/private/src/main/kotlin/androidx/build/clang/KonanBuildService.kt
+++ b/buildSrc/private/src/main/kotlin/androidx/build/clang/KonanBuildService.kt
@@ -33,6 +33,7 @@
 import org.gradle.process.ExecSpec
 import org.jetbrains.kotlin.gradle.plugin.KotlinMultiplatformPluginWrapper
 import org.jetbrains.kotlin.gradle.utils.NativeCompilerDownloader
+import org.jetbrains.kotlin.konan.target.Family
 import org.jetbrains.kotlin.konan.target.LinkerOutputKind
 import org.jetbrains.kotlin.konan.target.Platform
 import org.jetbrains.kotlin.konan.target.PlatformManager
@@ -124,13 +125,19 @@
         outputFile.parentFile.mkdirs()
 
         val platform = getPlatform(parameters.konanTarget)
+
+        // Specify max-page-size to align ELF regions to 16kb
+        val linkerFlags = if (parameters.konanTarget.get().asKonanTarget.family == Family.ANDROID) {
+            listOf("-z", "max-page-size=16384")
+        } else emptyList()
+
         val objectFiles = parameters.objectFiles.regularFilePaths()
         val linkedObjectFiles = parameters.linkedObjects.regularFilePaths()
         val linkCommands = platform.linker.finalLinkCommands(
             objectFiles = objectFiles,
             executable = outputFile.canonicalPath,
             libraries = linkedObjectFiles,
-            linkerArgs = emptyList(),
+            linkerArgs = linkerFlags,
             optimize = true,
             debug = false,
             kind = LinkerOutputKind.DYNAMIC_LIBRARY,
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 e51c3ac..32d2d2a 100644
--- a/buildSrc/private/src/main/kotlin/androidx/build/dackka/DackkaTask.kt
+++ b/buildSrc/private/src/main/kotlin/androidx/build/dackka/DackkaTask.kt
@@ -112,9 +112,12 @@
     @get:[InputFile PathSensitive(PathSensitivity.NONE)]
     abstract val libraryMetadataFile: RegularFileProperty
 
-    // The base URL to create source links for classes, as a format string with placeholders for the
-    // file path and qualified class name.
+    // The base URLs to create source links for classes, functions, and properties, respectively, as
+    // format strings with placeholders for the file path and qualified class name, function name,
+    // or property name.
     @get:Input abstract val baseSourceLink: Property<String>
+    @get:Input abstract val baseFunctionSourceLink: Property<String>
+    @get:Input abstract val basePropertySourceLink: Property<String>
 
     private fun sourceSets(): List<DokkaInputModels.SourceSet> {
         val externalDocs =
@@ -216,6 +219,8 @@
                                         "libraryMetadataFilename" to
                                             libraryMetadataFile.get().toString(),
                                         "baseSourceLink" to baseSourceLink.get(),
+                                        "baseFunctionSourceLink" to baseFunctionSourceLink.get(),
+                                        "basePropertySourceLink" to basePropertySourceLink.get(),
                                         "annotationsNotToDisplay" to annotationsNotToDisplay.get(),
                                         "annotationsNotToDisplayJava" to
                                             annotationsNotToDisplayJava.get(),
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 f22d4af..c516f58 100644
--- a/buildSrc/private/src/main/kotlin/androidx/build/docs/AndroidXDocsImplPlugin.kt
+++ b/buildSrc/private/src/main/kotlin/androidx/build/docs/AndroidXDocsImplPlugin.kt
@@ -533,8 +533,12 @@
                     excludedPackagesForKotlin.set(emptySet())
                     libraryMetadataFile.set(getMetadataRegularFile(project))
                     projectStructureMetadataFile.set(mergedProjectMetadata)
-                    // See go/dackka-source-link for details on this link.
-                    baseSourceLink.set("https://cs.android.com/search?" + "q=file:%s+class:%s")
+                    // See go/dackka-source-link for details on these links.
+                    baseSourceLink.set("https://cs.android.com/search?q=file:%s+class:%s")
+                    baseFunctionSourceLink.set(
+                        "https://cs.android.com/search?q=file:%s+function:%s"
+                    )
+                    basePropertySourceLink.set("https://cs.android.com/search?q=file:%s+symbol:%s")
                     annotationsNotToDisplay.set(hiddenAnnotations)
                     annotationsNotToDisplayJava.set(hiddenAnnotationsJava)
                     annotationsNotToDisplayKotlin.set(hiddenAnnotationsKotlin)
diff --git a/buildSrc/public/src/main/kotlin/androidx/build/SdkHelper.kt b/buildSrc/public/src/main/kotlin/androidx/build/SdkHelper.kt
index f056d56..447a6f1 100644
--- a/buildSrc/public/src/main/kotlin/androidx/build/SdkHelper.kt
+++ b/buildSrc/public/src/main/kotlin/androidx/build/SdkHelper.kt
@@ -81,24 +81,6 @@
     }
 }
 
-/** @return [File] representing the path stored in [envValue] if it exists, `null` otherwise. */
-private fun getPathFromEnvironmentVariableOrNull(envVar: String): File? {
-    val envValue = System.getenv(envVar)
-    if (envValue != null) {
-        val dir = File(envValue)
-        if (dir.isDirectory) {
-            return dir
-        }
-    }
-
-    return null
-}
-
-private fun fileIfExistsOrNull(parent: File, child: String): File? {
-    val file = File(parent, child)
-    return if (file.exists()) file else null
-}
-
 private fun getSdkPathFromEnvironmentVariable(): File {
     // check for environment variables, in the order AGP checks
     listOf("ANDROID_HOME", "ANDROID_SDK_ROOT").forEach {
@@ -132,12 +114,6 @@
     return extension.get("supportRootFolder") as File
 }
 
-/** Returns whether the path to the canonical root project directory has been set. */
-fun Project.hasSupportRootFolder(): Boolean {
-    val extension = project.rootProject.property("ext") as ExtraPropertiesExtension
-    return extension.has("supportRootFolder")
-}
-
 /**
  * Returns the path to the checkout's root directory, e.g. where {@code repo init} was run.
  *
diff --git a/busytown/androidx_multiplatform_mac.sh b/busytown/androidx_multiplatform_mac.sh
index f34ad7a..9bf3d6b 100755
--- a/busytown/androidx_multiplatform_mac.sh
+++ b/busytown/androidx_multiplatform_mac.sh
@@ -12,13 +12,14 @@
 # disable GCP cache, these machines don't have credentials.
 export USE_ANDROIDX_REMOTE_BUILD_CACHE=false
 
+sharedArgs="--no-configuration-cache -Pandroidx.constraints=true $*"
 # Setup simulators
 impl/androidx-native-mac-simulator-setup.sh
 
-impl/build.sh buildOnServer listTaskOutputs --no-configuration-cache createAllArchives -Pandroidx.constraints=true
+impl/build.sh buildOnServer listTaskOutputs createAllArchives $sharedArgs
 
 # run a separate createAllArchives task to prepare a repository
 # folder in DIST.
 # This cannot be merged with the buildOnServer run because
 # snapshot version is not a proper release version.
-DIST_DIR=$DIST_DIR/snapshots SNAPSHOT=true impl/build.sh createAllArchives --no-configuration-cache -Pandroidx.constraints=true
+DIST_DIR=$DIST_DIR/snapshots SNAPSHOT=true impl/build.sh createAllArchives $sharedArgs
diff --git a/busytown/androidx_multiplatform_mac_arm64.sh b/busytown/androidx_multiplatform_mac_arm64.sh
new file mode 100755
index 0000000..abb492b
--- /dev/null
+++ b/busytown/androidx_multiplatform_mac_arm64.sh
@@ -0,0 +1,5 @@
+#!/bin/bash
+set -e
+
+SCRIPT_DIR="$(cd $(dirname $0) && pwd)"
+$SCRIPT_DIR/androidx_multiplatform_mac.sh -Pandroidx.lowMemory
diff --git a/busytown/androidx_multiplatform_mac_host_tests.sh b/busytown/androidx_multiplatform_mac_host_tests.sh
index 9e2fcdf..6ee8947 100755
--- a/busytown/androidx_multiplatform_mac_host_tests.sh
+++ b/busytown/androidx_multiplatform_mac_host_tests.sh
@@ -8,9 +8,6 @@
 
 export ANDROIDX_PROJECTS=INFRAROGUE   # TODO: Switch from `INFRAROGUE` to `KMP`
 
-# disable GCP cache, these machines don't have credentials.
-export USE_ANDROIDX_REMOTE_BUILD_CACHE=false
-
 echo "Starting $0 at $(date)"
 
 cd "$(dirname $0)"
diff --git a/camera/camera-camera2-pipe-integration/src/main/java/androidx/camera/camera2/pipe/integration/impl/AndroidCaptureFailure.kt b/camera/camera-camera2-pipe-integration/src/main/java/androidx/camera/camera2/pipe/integration/impl/AndroidCaptureFailure.kt
deleted file mode 100644
index 083412f..0000000
--- a/camera/camera-camera2-pipe-integration/src/main/java/androidx/camera/camera2/pipe/integration/impl/AndroidCaptureFailure.kt
+++ /dev/null
@@ -1,47 +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.camera2.pipe.integration.impl
-
-import android.hardware.camera2.CaptureFailure
-import android.os.Build
-import androidx.annotation.RequiresApi
-import androidx.camera.camera2.pipe.FrameNumber
-import androidx.camera.camera2.pipe.RequestFailure
-import androidx.camera.camera2.pipe.RequestMetadata
-import androidx.camera.camera2.pipe.UnsafeWrapper
-import kotlin.reflect.KClass
-
-/**
- * This class implements the [RequestFailure] interface by passing the package-private
- * [CaptureFailure] object.
- */
-@RequiresApi(Build.VERSION_CODES.LOLLIPOP)
-internal class AndroidCaptureFailure(
-    override val requestMetadata: RequestMetadata,
-    override val captureFailure: CaptureFailure
-) : RequestFailure, UnsafeWrapper {
-    override val frameNumber: FrameNumber = FrameNumber(captureFailure.frameNumber)
-    override val reason: Int = captureFailure.reason
-    override val wasImageCaptured: Boolean = captureFailure.wasImageCaptured()
-
-    @Suppress("UNCHECKED_CAST")
-    override fun <T : Any> unwrapAs(type: KClass<T>): T? =
-        when (type) {
-            CaptureFailure::class -> captureFailure as T?
-            else -> null
-        }
-}
diff --git a/camera/camera-camera2-pipe-integration/src/main/java/androidx/camera/camera2/pipe/integration/impl/CameraCallbackMap.kt b/camera/camera-camera2-pipe-integration/src/main/java/androidx/camera/camera2/pipe/integration/impl/CameraCallbackMap.kt
index 1c71936..10ac90e 100644
--- a/camera/camera-camera2-pipe-integration/src/main/java/androidx/camera/camera2/pipe/integration/impl/CameraCallbackMap.kt
+++ b/camera/camera-camera2-pipe-integration/src/main/java/androidx/camera/camera2/pipe/integration/impl/CameraCallbackMap.kt
@@ -17,6 +17,7 @@
 package androidx.camera.camera2.pipe.integration.impl
 
 import android.hardware.camera2.CameraCaptureSession
+import android.hardware.camera2.CaptureFailure
 import android.hardware.camera2.CaptureRequest
 import android.hardware.camera2.CaptureResult
 import android.hardware.camera2.TotalCaptureResult
@@ -138,7 +139,7 @@
                 val session: CameraCaptureSession? =
                     requestMetadata.unwrapAs(CameraCaptureSession::class)
                 val request: CaptureRequest? = requestMetadata.unwrapAs(CaptureRequest::class)
-                val captureFailure = requestFailure.captureFailure
+                val captureFailure = requestFailure.unwrapAs(CaptureFailure::class)
                 if (session != null && request != null && captureFailure != null) {
                     executor.execute {
                         callback.captureCallback.onCaptureFailed(
diff --git a/camera/camera-camera2-pipe-integration/src/main/java/androidx/camera/camera2/pipe/integration/impl/FakeCaptureFailure.kt b/camera/camera-camera2-pipe-integration/src/main/java/androidx/camera/camera2/pipe/integration/impl/FakeCaptureFailure.kt
deleted file mode 100644
index d7d309b6..0000000
--- a/camera/camera-camera2-pipe-integration/src/main/java/androidx/camera/camera2/pipe/integration/impl/FakeCaptureFailure.kt
+++ /dev/null
@@ -1,37 +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.camera2.pipe.integration.impl
-
-import android.hardware.camera2.CaptureFailure
-import android.os.Build
-import androidx.annotation.RequiresApi
-import androidx.camera.camera2.pipe.FrameNumber
-import androidx.camera.camera2.pipe.RequestFailure
-import androidx.camera.camera2.pipe.RequestMetadata
-
-/**
- * This class implements the [RequestFailure] interface by extracting the fields of
- * the package-private [CaptureFailure] object.
- */
-@RequiresApi(Build.VERSION_CODES.LOLLIPOP)
-internal data class FakeCaptureFailure(
-    override val requestMetadata: RequestMetadata,
-    override val wasImageCaptured: Boolean,
-    override val frameNumber: FrameNumber,
-    override val reason: Int,
-    override val captureFailure: CaptureFailure?
-) : RequestFailure
diff --git a/camera/camera-camera2-pipe-integration/src/test/java/androidx/camera/camera2/pipe/integration/impl/CapturePipelineTest.kt b/camera/camera-camera2-pipe-integration/src/test/java/androidx/camera/camera2/pipe/integration/impl/CapturePipelineTest.kt
index 255514d..2b3445c 100644
--- a/camera/camera-camera2-pipe-integration/src/test/java/androidx/camera/camera2/pipe/integration/impl/CapturePipelineTest.kt
+++ b/camera/camera-camera2-pipe-integration/src/test/java/androidx/camera/camera2/pipe/integration/impl/CapturePipelineTest.kt
@@ -20,7 +20,6 @@
 import android.hardware.camera2.CameraCharacteristics
 import android.hardware.camera2.CameraDevice
 import android.hardware.camera2.CameraMetadata.CONTROL_AE_MODE_ON_ALWAYS_FLASH
-import android.hardware.camera2.CaptureFailure
 import android.hardware.camera2.CaptureRequest
 import android.hardware.camera2.CaptureRequest.CONTROL_AE_MODE
 import android.hardware.camera2.CaptureResult
@@ -60,6 +59,7 @@
 import androidx.camera.camera2.pipe.testing.FakeCameraMetadata
 import androidx.camera.camera2.pipe.testing.FakeFrameInfo
 import androidx.camera.camera2.pipe.testing.FakeFrameMetadata
+import androidx.camera.camera2.pipe.testing.FakeRequestFailure
 import androidx.camera.camera2.pipe.testing.FakeRequestMetadata
 import androidx.camera.core.ImageCapture
 import androidx.camera.core.ImageCaptureException
@@ -92,7 +92,6 @@
 import org.junit.Rule
 import org.junit.Test
 import org.junit.runner.RunWith
-import org.mockito.Mockito.mock
 import org.robolectric.annotation.Config
 import org.robolectric.annotation.internal.DoNotInstrument
 import org.robolectric.shadows.StreamConfigurationMapBuilder
@@ -658,12 +657,13 @@
                 // Callback capture fail immediately.
                 request.listeners.forEach {
                     val requestMetadata = FakeRequestMetadata()
+                    val frameNumber = FrameNumber(100L)
                     it.onFailed(
                         requestMetadata = requestMetadata,
-                        frameNumber = FrameNumber(100L),
-                        requestFailure = AndroidCaptureFailure(
+                        frameNumber = frameNumber,
+                        requestFailure = FakeRequestFailure(
                             requestMetadata,
-                            mock(CaptureFailure::class.java)
+                            frameNumber
                         )
                     )
                 }
diff --git a/camera/camera-camera2-pipe-integration/src/test/java/androidx/camera/camera2/pipe/integration/impl/StillCaptureRequestTest.kt b/camera/camera-camera2-pipe-integration/src/test/java/androidx/camera/camera2/pipe/integration/impl/StillCaptureRequestTest.kt
index d26b2a8..34973a8 100644
--- a/camera/camera-camera2-pipe-integration/src/test/java/androidx/camera/camera2/pipe/integration/impl/StillCaptureRequestTest.kt
+++ b/camera/camera-camera2-pipe-integration/src/test/java/androidx/camera/camera2/pipe/integration/impl/StillCaptureRequestTest.kt
@@ -16,7 +16,6 @@
 
 package androidx.camera.camera2.pipe.integration.impl
 
-import android.hardware.camera2.CaptureFailure
 import android.os.Build
 import androidx.camera.camera2.pipe.FrameNumber
 import androidx.camera.camera2.pipe.StreamId
@@ -32,6 +31,7 @@
 import androidx.camera.camera2.pipe.integration.testing.FakeSurface
 import androidx.camera.camera2.pipe.integration.testing.FakeUseCaseCamera
 import androidx.camera.camera2.pipe.testing.FakeFrameInfo
+import androidx.camera.camera2.pipe.testing.FakeRequestFailure
 import androidx.camera.camera2.pipe.testing.FakeRequestMetadata
 import androidx.camera.core.ImageCapture
 import androidx.camera.core.ImageCapture.CAPTURE_MODE_MINIMIZE_LATENCY
@@ -209,16 +209,12 @@
 
         fakeCameraGraphSession.submittedRequests.first().let { request ->
             request.listeners.forEach { listener ->
-                @Suppress("DEPRECATION")
                 listener.onFailed(
                     fakeRequestMetadata,
                     frameNumber,
-                    FakeCaptureFailure(
+                    FakeRequestFailure(
                         fakeRequestMetadata,
-                        false,
-                        frameNumber,
-                        CaptureFailure.REASON_ERROR,
-                        null
+                        frameNumber
                     )
                 )
             }
diff --git a/camera/camera-camera2-pipe-integration/src/test/java/androidx/camera/camera2/pipe/integration/testing/FakeCameraGraphSession.kt b/camera/camera-camera2-pipe-integration/src/test/java/androidx/camera/camera2/pipe/integration/testing/FakeCameraGraphSession.kt
index cd6027d..35d338b 100644
--- a/camera/camera-camera2-pipe-integration/src/test/java/androidx/camera/camera2/pipe/integration/testing/FakeCameraGraphSession.kt
+++ b/camera/camera-camera2-pipe-integration/src/test/java/androidx/camera/camera2/pipe/integration/testing/FakeCameraGraphSession.kt
@@ -29,11 +29,11 @@
 import androidx.camera.camera2.pipe.Request
 import androidx.camera.camera2.pipe.Result3A
 import androidx.camera.camera2.pipe.TorchState
-import androidx.camera.camera2.pipe.integration.impl.FakeCaptureFailure
 import androidx.camera.camera2.pipe.integration.testing.FakeCameraGraphSession.RequestStatus.ABORTED
 import androidx.camera.camera2.pipe.integration.testing.FakeCameraGraphSession.RequestStatus.FAILED
 import androidx.camera.camera2.pipe.integration.testing.FakeCameraGraphSession.RequestStatus.TOTAL_CAPTURE_DONE
 import androidx.camera.camera2.pipe.testing.FakeFrameInfo
+import androidx.camera.camera2.pipe.testing.FakeRequestFailure
 import androidx.camera.camera2.pipe.testing.FakeRequestMetadata
 import java.util.concurrent.Semaphore
 import kotlinx.coroutines.CompletableDeferred
@@ -180,12 +180,9 @@
                 FAILED -> listener.onFailed(
                     requestMetadata,
                     FrameNumber(0),
-                    FakeCaptureFailure(
+                    FakeRequestFailure(
                         requestMetadata,
-                        false,
-                        FrameNumber(0),
-                        CaptureFailure.REASON_ERROR,
-                        null
+                        FrameNumber(0)
                     )
                 )
 
diff --git a/camera/camera-camera2-pipe-testing/src/main/java/androidx/camera/camera2/pipe/testing/FakeRequestFailure.kt b/camera/camera-camera2-pipe-testing/src/main/java/androidx/camera/camera2/pipe/testing/FakeRequestFailure.kt
new file mode 100644
index 0000000..5660d9b
--- /dev/null
+++ b/camera/camera-camera2-pipe-testing/src/main/java/androidx/camera/camera2/pipe/testing/FakeRequestFailure.kt
@@ -0,0 +1,37 @@
+/*
+ * Copyright 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.camera.camera2.pipe.testing
+
+import androidx.camera.camera2.pipe.FrameNumber
+import androidx.camera.camera2.pipe.RequestFailure
+import androidx.camera.camera2.pipe.RequestMetadata
+import kotlin.reflect.KClass
+
+/**
+ * Utility class for testing code that depends on [RequestFailure] with reasonable defaults.
+ */
+class FakeRequestFailure(
+    override val requestMetadata: RequestMetadata,
+    override val frameNumber: FrameNumber,
+    override val reason: Int = 0,
+    override val wasImageCaptured: Boolean = false,
+) : RequestFailure {
+    override fun <T : Any> unwrapAs(type: KClass<T>): T? {
+        // Fake objects cannot be unwrapped.
+        return null
+    }
+}
diff --git a/camera/camera-camera2-pipe/src/main/java/androidx/camera/camera2/pipe/Frame.kt b/camera/camera-camera2-pipe/src/main/java/androidx/camera/camera2/pipe/Frame.kt
index 3e52406..9cc329c 100644
--- a/camera/camera-camera2-pipe/src/main/java/androidx/camera/camera2/pipe/Frame.kt
+++ b/camera/camera-camera2-pipe/src/main/java/androidx/camera/camera2/pipe/Frame.kt
@@ -171,11 +171,11 @@
 value class FrameId(val value: Long)
 
 /**
- * Represents the status of an output from the camera.
+ * Represents the status of an output from the camera with enum-like values.
  */
 @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
 @JvmInline
-value class OutputStatus(val value: Int) {
+value class OutputStatus internal constructor(val value: Int) {
     companion object {
         /** Output is not yet available. */
         val PENDING = OutputStatus(0)
@@ -258,7 +258,7 @@
     /**
      * Get the status of the pending [Frame].
      */
-    val frameStatus: OutputStatus
+    val status: OutputStatus
 
     /**
      * Get or suspend until the [Frame] that will be produced by the camera for this [request] is
diff --git a/camera/camera-camera2-pipe/src/main/java/androidx/camera/camera2/pipe/Requests.kt b/camera/camera-camera2-pipe/src/main/java/androidx/camera/camera2/pipe/Requests.kt
index db5f5d6..76585d3 100644
--- a/camera/camera-camera2-pipe/src/main/java/androidx/camera/camera2/pipe/Requests.kt
+++ b/camera/camera-camera2-pipe/src/main/java/androidx/camera/camera2/pipe/Requests.kt
@@ -261,16 +261,22 @@
  * constructor prevents directly creating an instance of it.
  */
 @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
-interface RequestFailure {
+interface RequestFailure : UnsafeWrapper {
+    /** Metadata about the request that has failed. */
     val requestMetadata: RequestMetadata
 
+    /** The Camera [FrameNumber] for the request that has failed. */
     val frameNumber: FrameNumber
 
+    /** Indicates the reason the particular request failed, see [CaptureFailure] for details. */
     val reason: Int
 
+    /**
+     * Indicates if images were still captured for this request. If this is true, the camera should
+     * invoke [Request.Listener.onBufferLost] individually for each output that failed. If this is
+     * false, these outputs will never arrive, and the individual callbacks will not be invoked.
+     */
     val wasImageCaptured: Boolean
-
-    val captureFailure: CaptureFailure?
 }
 
 /**
@@ -296,14 +302,12 @@
 
 /**
  * The intended use for this class is to submit the input needed for a reprocessing request, the
- * [InputStream], [ImageWrapper] and [FrameMetadata]. Both values are non-nullable because
+ * [ImageWrapper] and [FrameInfo]. Both values are non-nullable because
  * both values are needed for reprocessing.
  */
 @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
 data class InputRequest(
-    val inputStreamId: InputStreamId,
     val image: ImageWrapper,
-    val frameMetadata: FrameMetadata,
     val frameInfo: FrameInfo
 )
 
diff --git a/camera/camera-camera2-pipe/src/main/java/androidx/camera/camera2/pipe/compat/AndroidCaptureFailure.kt b/camera/camera-camera2-pipe/src/main/java/androidx/camera/camera2/pipe/compat/AndroidCaptureFailure.kt
index 9cdf28fc..ae96642 100644
--- a/camera/camera-camera2-pipe/src/main/java/androidx/camera/camera2/pipe/compat/AndroidCaptureFailure.kt
+++ b/camera/camera-camera2-pipe/src/main/java/androidx/camera/camera2/pipe/compat/AndroidCaptureFailure.kt
@@ -24,7 +24,6 @@
 import androidx.camera.camera2.pipe.FrameNumber
 import androidx.camera.camera2.pipe.RequestFailure
 import androidx.camera.camera2.pipe.RequestMetadata
-import androidx.camera.camera2.pipe.UnsafeWrapper
 import kotlin.reflect.KClass
 
 /**
@@ -34,8 +33,8 @@
 @RequiresApi(Build.VERSION_CODES.LOLLIPOP)
 class AndroidCaptureFailure(
     override val requestMetadata: RequestMetadata,
-    override val captureFailure: CaptureFailure
-) : RequestFailure, UnsafeWrapper {
+    private val captureFailure: CaptureFailure
+) : RequestFailure {
     override val frameNumber: FrameNumber = FrameNumber(captureFailure.frameNumber)
     override val reason: Int = captureFailure.reason
     override val wasImageCaptured: Boolean = captureFailure.wasImageCaptured()
diff --git a/camera/camera-camera2-pipe/src/main/java/androidx/camera/camera2/pipe/compat/Camera2CaptureSequence.kt b/camera/camera-camera2-pipe/src/main/java/androidx/camera/camera2/pipe/compat/Camera2CaptureSequence.kt
index 49cb8ce..5fd7fcc 100644
--- a/camera/camera-camera2-pipe/src/main/java/androidx/camera/camera2/pipe/compat/Camera2CaptureSequence.kt
+++ b/camera/camera-camera2-pipe/src/main/java/androidx/camera/camera2/pipe/compat/Camera2CaptureSequence.kt
@@ -194,17 +194,16 @@
 
         // Load the request and throw if we are not able to find an associated request. Under
         // normal circumstances this should never happen.
-        val request = readRequestMetadata(requestNumber)
+        val requestMetadata = readRequestMetadata(requestNumber)
 
-        val simpleCaptureFailure = SimpleCaptureFailure(
-            request,
+        val extensionRequestFailure = ExtensionRequestFailure(
+            requestMetadata,
             false,
             frameNumber,
-            CaptureFailure.REASON_ERROR,
-            null
+            CaptureFailure.REASON_ERROR
         )
 
-        invokeCaptureFailure(request, frameNumber, simpleCaptureFailure)
+        invokeCaptureFailure(requestMetadata, frameNumber, extensionRequestFailure)
     }
 
     override fun onCaptureBufferLost(
diff --git a/camera/camera-camera2-pipe/src/main/java/androidx/camera/camera2/pipe/compat/SimpleCaptureFailure.kt b/camera/camera-camera2-pipe/src/main/java/androidx/camera/camera2/pipe/compat/ExtensionRequestFailure.kt
similarity index 88%
rename from camera/camera-camera2-pipe/src/main/java/androidx/camera/camera2/pipe/compat/SimpleCaptureFailure.kt
rename to camera/camera-camera2-pipe/src/main/java/androidx/camera/camera2/pipe/compat/ExtensionRequestFailure.kt
index 2d43ff8..ee372c5 100644
--- a/camera/camera-camera2-pipe/src/main/java/androidx/camera/camera2/pipe/compat/SimpleCaptureFailure.kt
+++ b/camera/camera-camera2-pipe/src/main/java/androidx/camera/camera2/pipe/compat/ExtensionRequestFailure.kt
@@ -24,16 +24,20 @@
 import androidx.camera.camera2.pipe.FrameNumber
 import androidx.camera.camera2.pipe.RequestFailure
 import androidx.camera.camera2.pipe.RequestMetadata
+import kotlin.reflect.KClass
 
 /**
  * This class implements the [RequestFailure] interface by extracting the fields of
  * the package-private [CaptureFailure] object.
  */
 @RequiresApi(Build.VERSION_CODES.LOLLIPOP)
-internal data class SimpleCaptureFailure(
+internal data class ExtensionRequestFailure(
     override val requestMetadata: RequestMetadata,
     override val wasImageCaptured: Boolean,
     override val frameNumber: FrameNumber,
     override val reason: Int,
-    override val captureFailure: CaptureFailure?
-) : RequestFailure
+) : RequestFailure {
+    override fun <T : Any> unwrapAs(type: KClass<T>): T? {
+        return null
+    }
+}
diff --git a/camera/camera-camera2-pipe/src/main/java/androidx/camera/camera2/pipe/internal/FrameCaptureQueue.kt b/camera/camera-camera2-pipe/src/main/java/androidx/camera/camera2/pipe/internal/FrameCaptureQueue.kt
new file mode 100644
index 0000000..32c889d
--- /dev/null
+++ b/camera/camera-camera2-pipe/src/main/java/androidx/camera/camera2/pipe/internal/FrameCaptureQueue.kt
@@ -0,0 +1,202 @@
+/*
+ * Copyright 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.camera.camera2.pipe.internal
+
+import android.os.Build
+import androidx.annotation.GuardedBy
+import androidx.annotation.RequiresApi
+import androidx.camera.camera2.pipe.Frame
+import androidx.camera.camera2.pipe.FrameCapture
+import androidx.camera.camera2.pipe.OutputStatus
+import androidx.camera.camera2.pipe.Request
+import androidx.camera.camera2.pipe.config.CameraGraphScope
+import androidx.camera.camera2.pipe.internal.OutputResult.Companion.completeWithFailure
+import androidx.camera.camera2.pipe.internal.OutputResult.Companion.completeWithOutput
+import androidx.camera.camera2.pipe.internal.OutputResult.Companion.outputOrNull
+import androidx.camera.camera2.pipe.internal.OutputResult.Companion.outputStatus
+import javax.inject.Inject
+import kotlinx.atomicfu.atomic
+import kotlinx.coroutines.CompletableDeferred
+import kotlinx.coroutines.ExperimentalCoroutinesApi
+
+/**
+ * FrameCaptureQueue manages the list of requests that are expected to produce a [Frame] that needs
+ * to be returned when the [Frame] that matches the [Request] is started.
+ */
+@RequiresApi(Build.VERSION_CODES.LOLLIPOP)
+@CameraGraphScope
+internal class FrameCaptureQueue @Inject constructor() : AutoCloseable {
+    private val lock = Any()
+
+    @GuardedBy("lock")
+    private val queue = ArrayDeque<FrameCaptureImpl>()
+
+    @GuardedBy("lock")
+    private var closed = false
+
+    fun remove(request: Request): FrameCaptureImpl? =
+        synchronized(lock) {
+            if (closed) return null
+
+            // If an item matching this request exists, remove it from the queue and return it.
+            queue.firstOrNull {
+                it.request == request
+            }?.also { queue.remove(it) }
+        }
+
+    /**
+     * Tell the [FrameDistributor] that a specific request will be submitted to the camera and
+     * create a placeholder that will be completed when that specific request starts exposing.
+     */
+    fun enqueue(request: Request): FrameCaptureImpl =
+        synchronized(lock) {
+            FrameCaptureImpl(request).also {
+                if (!closed) {
+                    queue.add(it)
+                } else {
+                    it.close()
+                }
+            }
+        }
+
+    /**
+     * Tell the [FrameDistributor] that a specific list of requests will be submitted to the
+     * camera and to create placeholders.
+     */
+    fun enqueue(requests: List<Request>): List<FrameCapture> =
+        synchronized(lock) {
+            requests.map { FrameCaptureImpl(it) }.also {
+                if (!closed) {
+                    queue.addAll(it)
+                } else {
+                    for (result in it) {
+                        result.close()
+                    }
+                }
+            }
+        }
+
+    override fun close() {
+        synchronized(lock) {
+            if (closed) return
+            closed = true
+        }
+
+        // Note: This happens outside the synchronized block, but is safe since all modifications
+        // above happen within the synchronized block, and all modifications check the closed value
+        // before modifying the list.
+        for (pendingOutputFrame in queue) {
+
+            // Any pending frame in the queue is guaranteed to not hold a real result.
+            pendingOutputFrame.completeWithFailure(OutputStatus.ERROR_OUTPUT_ABORTED)
+        }
+        queue.clear()
+    }
+
+    @OptIn(ExperimentalCoroutinesApi::class)
+    internal inner class FrameCaptureImpl(override val request: Request) : FrameCapture {
+        private val closed = atomic(false)
+        private val result = CompletableDeferred<OutputResult<Frame>>()
+
+        @GuardedBy("this")
+        private var frameListeners: MutableList<Frame.Listener>? = mutableListOf()
+
+        /** Complete this [FrameCapture] with the provide [Frame]. */
+        fun completeWith(frame: Frame) {
+            if (!result.completeWithOutput(frame)) {
+                // Close the frame if the result was non-null and we failed to complete this frame
+                frame.close()
+            } else {
+                val listeners: List<Frame.Listener>?
+                synchronized(this) {
+                    listeners = frameListeners
+                    frameListeners = null
+                }
+
+                if (listeners != null) {
+                    for (i in listeners.indices) {
+                        frame.addListener(listeners[i])
+                    }
+                }
+            }
+        }
+
+        /** Cancel this [FrameCapture] with a specific [OutputStatus]. */
+        fun completeWithFailure(failureStatus: OutputStatus) {
+            if (result.completeWithFailure(failureStatus)) {
+                val listeners: List<Frame.Listener>?
+                synchronized(this) {
+                    listeners = frameListeners
+                    frameListeners = null
+                }
+
+                // Ensure listeners always receive the onFrameCompleted event, since it will not be
+                // attached to a real frame.
+                if (listeners != null) {
+                    for (i in listeners.indices) {
+                        listeners[i].onFrameComplete()
+                    }
+                }
+            }
+        }
+
+        override fun getFrame(): Frame? {
+            if (closed.value) return null
+            return result.outputOrNull()?.tryAcquire()
+        }
+
+        override val status: OutputStatus
+            get() {
+                if (closed.value) return OutputStatus.UNAVAILABLE
+                return result.outputStatus()
+            }
+
+        override suspend fun awaitFrame(): Frame? {
+            if (closed.value) return null
+            return result.await().output?.tryAcquire()
+        }
+
+        override fun addListener(listener: Frame.Listener) {
+            val success = synchronized(this) {
+                frameListeners?.add(listener) == true
+            }
+            // If the list of listeners is null, then we've already completed this deferred output
+            // frame.
+            if (!success) {
+                val frame = result.outputOrNull()
+                if (frame != null) {
+                    frame.addListener(listener)
+                } else {
+                    listener.onFrameComplete()
+                }
+            }
+        }
+
+        override fun close() {
+            if (closed.compareAndSet(expect = false, update = true)) {
+                completeWithFailure(OutputStatus.UNAVAILABLE)
+                result.outputOrNull()?.close()
+
+                // We should close all of the object if we successfully remove it from the list.
+                // Otherwise, this operation is a no-op.
+                synchronized(lock) {
+                    queue.remove(this)
+                }
+            }
+        }
+    }
+}
diff --git a/camera/camera-camera2-pipe/src/main/java/androidx/camera/camera2/pipe/internal/FrameDistributor.kt b/camera/camera-camera2-pipe/src/main/java/androidx/camera/camera2/pipe/internal/FrameDistributor.kt
new file mode 100644
index 0000000..e88f31e5
--- /dev/null
+++ b/camera/camera-camera2-pipe/src/main/java/androidx/camera/camera2/pipe/internal/FrameDistributor.kt
@@ -0,0 +1,251 @@
+/*
+ * Copyright 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.camera.camera2.pipe.internal
+
+import android.os.Build
+import androidx.annotation.RequiresApi
+import androidx.camera.camera2.pipe.CameraTimestamp
+import androidx.camera.camera2.pipe.Frame
+import androidx.camera.camera2.pipe.FrameCapture
+import androidx.camera.camera2.pipe.FrameInfo
+import androidx.camera.camera2.pipe.FrameNumber
+import androidx.camera.camera2.pipe.FrameReference
+import androidx.camera.camera2.pipe.OutputStatus
+import androidx.camera.camera2.pipe.Request
+import androidx.camera.camera2.pipe.RequestFailure
+import androidx.camera.camera2.pipe.RequestMetadata
+import androidx.camera.camera2.pipe.StreamId
+import androidx.camera.camera2.pipe.media.ClosingFinalizer
+import androidx.camera.camera2.pipe.media.ImageSource
+import androidx.camera.camera2.pipe.media.NoOpFinalizer
+import androidx.camera.camera2.pipe.media.OutputImage
+
+/**
+ * A FrameDistributor is responsible for listening to events from each [Request] as well as images
+ * that are produced by [ImageSources][ImageSource] in order to group them into [Frames][Frame] and
+ * distribute these frames to downstream consumers.
+ *
+ * Frames can be safely held until needed, passed to other consumers, or closed at any point in
+ * time while correctly handling the underlying resource counts and error handling, which is a
+ * non-trivial problem to solve correctly and efficiently. For optimal behavior, an instance of this
+ * class should be attached as a listener to the callbacks of *all* [Requests][Request] sent to the
+ * Camera.
+ *
+ * Frames are distributed during [onStarted] in two primary ways:
+ *
+ *   1. To [FrameCapture] for non-repeating capture requests in the [frameCaptureQueue]
+ *   2. To the [frameStartedListener] as a [FrameReference], which must call [Frame.tryAcquire] if
+ *      it would like to hold onto it beyond the lifetime of the method call.
+ *
+ * The remaining callbacks are used to distribute specific error and failure conditions to frames
+ * that were previously started.
+ */
+@RequiresApi(Build.VERSION_CODES.LOLLIPOP)
+internal class FrameDistributor(
+    imageSources: Map<StreamId, ImageSource>,
+    private val frameCaptureQueue: FrameCaptureQueue,
+    private val frameStartedListener: FrameStartedListener
+) : AutoCloseable, Request.Listener {
+    /**
+     * Listener to observe new [FrameReferences][FrameReference] as they are started by the camera.
+     */
+    fun interface FrameStartedListener {
+        /**
+         * Invoked when a new [Frame] has started. Implementations must synchronously invoke
+         * [Frame.tryAcquire] if they want to maintain a valid reference to the [Frame]
+         */
+        fun onFrameStarted(frameReference: FrameReference)
+    }
+
+    private val frameInfoDistributor = OutputDistributor<FrameInfo>(outputFinalizer = NoOpFinalizer)
+
+    // This is an example CameraGraph configuration a camera configured with both capture and
+    // non capture output streams, as well as physical outputs:
+    //
+    // Camera-0 (Logical)
+    //   Stream-1  Output-1   Viewfinder  (No Images)
+    //   Stream-2  Output-2   YUV_420
+    //   Stream-3  Output-3   RAW10       [Camera-4]
+    //             Output-4   RAW10       [Camera-5]
+    //             Output-5   RAW10       [Camera-6]
+    //
+    // In this scenario the FrameDistributor will handle distributing images to Stream-2 and
+    // to Stream-3 if they are configured with an ImageSource. Each of these streams is
+    // associated with its own OutputDistributor for error handling and grouping.
+    private val imageDistributors = imageSources.mapValues { (_, imageSource) ->
+        val imageDistributor =
+            OutputDistributor<OutputImage>(outputFinalizer = ClosingFinalizer)
+
+        // Bind the listener on the ImageSource to the imageDistributor. This listener
+        // and the imageDistributor may be invoked on a different thread.
+        imageSource.setListener { imageStreamId, imageOutputId, outputTimestamp, image ->
+            if (image != null) {
+                imageDistributor.onOutputResult(
+                    outputTimestamp,
+                    OutputResult.from(OutputImage.from(imageStreamId, imageOutputId, image))
+                )
+            } else {
+                imageDistributor.onOutputResult(
+                    outputTimestamp,
+                    OutputResult.failure(OutputStatus.ERROR_OUTPUT_DROPPED)
+                )
+            }
+        }
+
+        imageDistributor
+    }
+    private val imageStreams: Set<StreamId> = imageDistributors.keys
+
+    /**
+     * Create and distribute a [Frame] to the pending [FrameCapture] (If one has been registered
+     * for this request), and to the [FrameStartedListener]
+     */
+    override fun onStarted(
+        requestMetadata: RequestMetadata,
+        frameNumber: FrameNumber,
+        timestamp: CameraTimestamp
+    ) {
+        // When the camera begins exposing a frame, create a placeholder for all of the outputs that
+        // will be produced, and tell each of the image distributors to expect results for this
+        // frameNumber and timestamp.
+        val frameState = FrameState(
+            requestMetadata,
+            frameNumber,
+            timestamp,
+            imageStreams
+        )
+
+        // Tell the frameInfo distributor to expect FrameInfo at the provided FrameNumber
+        frameInfoDistributor.onOutputStarted(
+            cameraFrameNumber = frameNumber,
+            cameraTimestamp = timestamp,
+            outputNumber = frameNumber.value, // Number to match output against
+            outputListener = frameState.frameInfoOutput
+        )
+
+        // Tell each imageDistributor to expect an Image at the provided CameraTimestamp.
+        for (i in frameState.imageOutputs.indices) {
+            val imageOutput = frameState.imageOutputs[i]
+            val imageDistributor = imageDistributors[imageOutput.streamId]!!
+
+            // Images are matched to the frame based on the cameraTimestamp.
+            imageDistributor.onOutputStarted(
+                cameraFrameNumber = frameNumber,
+                cameraTimestamp = timestamp,
+                outputNumber = timestamp.value, // Number to match output against
+                outputListener = imageOutput
+            )
+
+            if (!requestMetadata.streams.keys.contains(imageOutput.streamId)) {
+                // Edge case: It's possible that a CaptureRequest submitted to the camera
+                // is different than the Request used to create it in a few scenarios (such
+                // as the surface being unavailable or invalid). If this happens, tell the
+                // imageDistributor that the output has failed for this specific frame.
+                imageDistributor.onOutputFailure(frameState.frameNumber)
+            }
+        }
+
+        // Create a Frame, and offer it
+        val frame = FrameImpl(frameState)
+        frameStartedListener.onFrameStarted(frame)
+
+        // If there is an explicit capture request associated with this request, pass it to the
+        // FrameCapture.
+        if (!requestMetadata.repeating) {
+            val frameCapture = frameCaptureQueue.remove(requestMetadata.request)
+            if (frameCapture != null) {
+                frameCapture.completeWith(frame)
+                return
+            }
+        }
+
+        // Close the frame. This releases the reference we are holding.
+        frame.close()
+    }
+
+    override fun onComplete(
+        requestMetadata: RequestMetadata,
+        frameNumber: FrameNumber,
+        result: FrameInfo
+    ) {
+        // Tell the frameInfo distributor that the metadata for this exposure has been computed and
+        // can be distributed.
+        frameInfoDistributor.onOutputResult(frameNumber.value, OutputResult.from(result))
+    }
+
+    override fun onBufferLost(
+        requestMetadata: RequestMetadata,
+        frameNumber: FrameNumber,
+        stream: StreamId
+    ) {
+        // Tell the specific image distributor for this stream that the output has failed and will
+        // not arrive for this frame. When onBufferLost occurs, other images and metadata may still
+        // complete successfully.
+        imageDistributors[stream]?.onOutputFailure(frameNumber)
+    }
+
+    override fun onFailed(
+        requestMetadata: RequestMetadata,
+        frameNumber: FrameNumber,
+        requestFailure: RequestFailure
+    ) {
+        // Metadata will not arrive for this frame:
+        frameInfoDistributor.onOutputResult(
+            frameNumber.value,
+            OutputResult.failure(OutputStatus.ERROR_OUTPUT_FAILED)
+        )
+
+        // There are two scenarios:
+        // 1. Images were captured: In this case, camera2 will send individual failures for each
+        //    failed image via the onBufferLost callback.
+        // 2. Images were *not* captured: (This case), if images were not captured, all outputs have
+        //    failed, and camera2 will not invoke onBufferLost. We are responsible for marking all
+        //    outputs as failed.
+        if (!requestFailure.wasImageCaptured) {
+            // Note: The actual streams used by camera2 are specified in requestMetadata.streams and
+            //   may be different than requestMetadata.request.streams if one of the surfaces was
+            //   not ready or available. Make sure we iterate over `requestMetadata.streams`
+            for (stream in requestMetadata.streams.keys) {
+                imageDistributors[stream]?.onOutputFailure(frameNumber)
+            }
+        }
+    }
+
+    override fun onAborted(request: Request) {
+        // When a request is aborted, it may (or may not) be in some stage of capture, and it might
+        // not have started yet. The only thing we know for sure is:
+        //
+        // 1. Only single requests can be aborted (repeating requests are never guaranteed to
+        //    produce outputs, so they never get aborted calls)
+        // 2. If a request is already started, the camera should handle the failures for aborted
+        //    captures as a separate set of events.
+        frameCaptureQueue.remove(request)?.completeWithFailure(OutputStatus.ERROR_OUTPUT_ABORTED)
+    }
+
+    override fun close() {
+        // Closing the captureQueue aborts all pending and future capture requests.
+        frameCaptureQueue.close()
+
+        // Stop distributing FrameInfo
+        frameInfoDistributor.close()
+
+        // Stop distributing Images
+        for (imageDistributor in imageDistributors.values) {
+            imageDistributor.close()
+        }
+    }
+}
diff --git a/camera/camera-camera2-pipe/src/main/java/androidx/camera/camera2/pipe/internal/FrameImpl.kt b/camera/camera-camera2-pipe/src/main/java/androidx/camera/camera2/pipe/internal/FrameImpl.kt
index 3408acb..7229043 100644
--- a/camera/camera-camera2-pipe/src/main/java/androidx/camera/camera2/pipe/internal/FrameImpl.kt
+++ b/camera/camera-camera2-pipe/src/main/java/androidx/camera/camera2/pipe/internal/FrameImpl.kt
@@ -141,7 +141,7 @@
 
     override fun getFrameInfo(): FrameInfo? {
         if (closed.value) return null
-        return frameState.frameInfoOutput.getCompletedOrNull()
+        return frameState.frameInfoOutput.outputOrNull()
     }
 
     override suspend fun awaitImage(streamId: StreamId): OutputImage? {
@@ -155,7 +155,7 @@
         if (closed.value) return null
         if (!imageStreams.contains(streamId)) return null
         val output = frameState.imageOutputs.firstOrNull { it.streamId == streamId }
-        return output?.getCompletedOrNull()
+        return output?.outputOrNull()
     }
 
     override fun imageStatus(streamId: StreamId): OutputStatus {
@@ -170,9 +170,4 @@
     }
 
     override fun toString(): String = frameState.toString()
-
-    companion object {
-        private val frameIds = atomic(0L)
-        internal fun nextFrameId(): FrameId = FrameId(frameIds.incrementAndGet())
-    }
 }
diff --git a/camera/camera-camera2-pipe/src/main/java/androidx/camera/camera2/pipe/internal/FrameState.kt b/camera/camera-camera2-pipe/src/main/java/androidx/camera/camera2/pipe/internal/FrameState.kt
index ba4603a..c0b30a7 100644
--- a/camera/camera-camera2-pipe/src/main/java/androidx/camera/camera2/pipe/internal/FrameState.kt
+++ b/camera/camera-camera2-pipe/src/main/java/androidx/camera/camera2/pipe/internal/FrameState.kt
@@ -30,14 +30,17 @@
 import androidx.camera.camera2.pipe.internal.FrameState.State.FRAME_INFO_COMPLETE
 import androidx.camera.camera2.pipe.internal.FrameState.State.STARTED
 import androidx.camera.camera2.pipe.internal.FrameState.State.STREAM_RESULTS_COMPLETE
-import androidx.camera.camera2.pipe.media.OutputDistributor
+import androidx.camera.camera2.pipe.internal.OutputResult.Companion.completeWithFailure
+import androidx.camera.camera2.pipe.internal.OutputResult.Companion.completeWithOutput
+import androidx.camera.camera2.pipe.internal.OutputResult.Companion.outputOrNull
+import androidx.camera.camera2.pipe.internal.OutputResult.Companion.outputStatus
 import androidx.camera.camera2.pipe.media.OutputImage
 import androidx.camera.camera2.pipe.media.SharedOutputImage
 import java.util.concurrent.CopyOnWriteArrayList
 import kotlinx.atomicfu.atomic
 import kotlinx.atomicfu.updateAndGet
 import kotlinx.coroutines.CompletableDeferred
-import kotlinx.coroutines.ExperimentalCoroutinesApi
+import kotlinx.coroutines.Deferred
 
 /**
  * This class represents a successfully started frame from the camera, and placeholders for the
@@ -46,11 +49,11 @@
 @RequiresApi(21)
 internal class FrameState(
     val requestMetadata: RequestMetadata,
-    val frameId: FrameId,
     val frameNumber: FrameNumber,
     val frameTimestamp: CameraTimestamp,
     imageStreams: Set<StreamId>
 ) {
+    val frameId = nextFrameId()
     val frameInfoOutput: FrameInfoOutput = FrameInfoOutput()
     val imageOutputs: List<ImageOutput> = buildList {
         for (streamId in requestMetadata.streams.keys) {
@@ -152,7 +155,6 @@
      * [FrameOutput] handles the logic and reference counting that is required to safely handle a
      * shared `CompletableDeferred` instance that may contain an expensive closable resource.
      */
-    @OptIn(ExperimentalCoroutinesApi::class)
     internal abstract class FrameOutput<T : Any> {
         private val count = atomic(1)
 
@@ -161,7 +163,8 @@
          * object of type T OR the [OutputStatus] that was passed down when this output was
          * completed.
          */
-        val result = CompletableDeferred<Any>()
+        protected val internalResult = CompletableDeferred<OutputResult<T>>()
+        val result: Deferred<OutputResult<T>> get() = internalResult
 
         fun increment(): Boolean {
             val current =
@@ -177,17 +180,10 @@
 
         fun decrement() {
             if (count.decrementAndGet() == 0) {
-                result.cancel()
-                try {
-                    if (!result.isCancelled) {
-                        // If we call cancel(), but the end state is not canceled, it means that
-                        // the result was previously completed successfully. In this case, we need
-                        // to release the underlying reference this Frame is holding on to.
-                        result.getCompleted().asOutput { release(it) }
-                    }
-                } catch (ignored: IllegalStateException) {
-                    // NoOp, this should never happen.
-                }
+                // UNAVAILABLE is used to indicate outputs that have been closed or released during
+                // normal operation.
+                internalResult.completeWithFailure(OutputStatus.UNAVAILABLE)
+                release()
             }
         }
 
@@ -195,53 +191,16 @@
             get() {
                 // A result of `isCancelled` indicates the frame was closed before the Output
                 // arrived.
-                if (count.value == 0 || result.isCancelled) {
+                if (count.value == 0) {
                     return OutputStatus.UNAVAILABLE
                 }
-
-                // If the result is not canceled, and not completed, then we are waiting for the
-                // output to arrive.
-                if (!result.isCompleted) {
-                    return OutputStatus.PENDING
-                }
-
-                // This is guaranteed to be completed.
-                val result = result.getCompleted()
-                if (result is OutputStatus) {
-                    return result
-                }
-                return OutputStatus.AVAILABLE
+                return internalResult.outputStatus()
             }
 
-        fun getCompletedOrNull(): T? =
-            if (!result.isCompleted || result.isCancelled) {
-                null
-            } else {
-                result.getCompleted().asOutput { acquire(it) }
-            }
+        abstract fun outputOrNull(): T?
+        abstract suspend fun await(): T?
 
-        protected fun completeWith(output: T?, outputResult: OutputStatus): Boolean {
-            val result = if (output == null) {
-                result.complete(outputResult)
-            } else {
-                result.complete(output)
-            }
-            return result
-        }
-
-        private inline fun <R> Any.asOutput(block: (T) -> R): R? {
-            if (this is OutputStatus) return null
-            @Suppress("UNCHECKED_CAST")
-            return block(this as T)
-        }
-
-        suspend fun await(): T? = result.await().asOutput { acquire(it) }
-
-        /** Invoked to acquire the underlying resource. */
-        protected abstract fun acquire(value: T): T?
-
-        /** Invoked when the underlying resource is no longer referenced and should be released. */
-        protected abstract fun release(value: T)
+        protected abstract fun release()
     }
 
     @RequiresApi(Build.VERSION_CODES.LOLLIPOP)
@@ -253,22 +212,23 @@
             cameraTimestamp: CameraTimestamp,
             outputSequence: Long,
             outputNumber: Long,
-            outputStatus: OutputStatus,
-            output: FrameInfo?
+            outputResult: OutputResult<FrameInfo>
         ) {
+            val output = outputResult.output
             check(output == null || output.frameNumber.value == outputNumber) {
                 "Unexpected FrameInfo: $output " +
                     "Expected ${output?.frameNumber?.value} to match $outputNumber!"
             }
 
-            completeWith(output, outputStatus)
+            internalResult.complete(outputResult)
             onFrameInfoComplete()
         }
 
-        override fun acquire(value: FrameInfo): FrameInfo = value
+        override suspend fun await(): FrameInfo? = result.await().output
+        override fun outputOrNull() = result.outputOrNull()
 
-        override fun release(value: FrameInfo) {
-            // Ignored: FrameInfo is not closable.
+        override fun release() {
+            // NoOp
         }
     }
 
@@ -281,23 +241,36 @@
             cameraTimestamp: CameraTimestamp,
             outputSequence: Long,
             outputNumber: Long,
-            outputStatus: OutputStatus,
-            output: OutputImage?
+            outputResult: OutputResult<OutputImage>
         ) {
-            check(output == null || output.timestamp == outputNumber) {
-                "Unexpected Image: $output, expected ${output?.timestamp} to match $outputNumber!"
+            val output = outputResult.output
+            if (output != null) {
+                check(output.timestamp == outputNumber) {
+                    "Unexpected image: $output! Expected ${output.timestamp} " +
+                        "to match $outputNumber!"
+                }
+                val sharedImage = SharedOutputImage.from(output)
+                if (!internalResult.completeWithOutput(sharedImage)) {
+                    sharedImage.close()
+                }
+            } else {
+                internalResult.completeWithFailure(outputResult.status)
             }
-            val sharedImage = output?.let { SharedOutputImage.from(it) }
-            if (!completeWith(sharedImage, outputStatus)) {
-                sharedImage?.close()
-            }
+
             onStreamResultComplete(streamId)
         }
 
-        override fun release(value: SharedOutputImage) {
-            value.close()
-        }
+        override fun outputOrNull(): SharedOutputImage? = result.outputOrNull()?.acquireOrNull()
 
-        override fun acquire(value: SharedOutputImage): SharedOutputImage? = value.acquireOrNull()
+        override suspend fun await(): SharedOutputImage? = result.await().output?.acquireOrNull()
+
+        override fun release() {
+            internalResult.outputOrNull()?.close()
+        }
+    }
+
+    companion object {
+        private val frameIds = atomic(0L)
+        private fun nextFrameId(): FrameId = FrameId(frameIds.incrementAndGet())
     }
 }
diff --git a/camera/camera-camera2-pipe/src/main/java/androidx/camera/camera2/pipe/media/OutputDistributor.kt b/camera/camera-camera2-pipe/src/main/java/androidx/camera/camera2/pipe/internal/OutputDistributor.kt
similarity index 78%
rename from camera/camera-camera2-pipe/src/main/java/androidx/camera/camera2/pipe/media/OutputDistributor.kt
rename to camera/camera-camera2-pipe/src/main/java/androidx/camera/camera2/pipe/internal/OutputDistributor.kt
index 30a68b1..91c4338 100644
--- a/camera/camera-camera2-pipe/src/main/java/androidx/camera/camera2/pipe/media/OutputDistributor.kt
+++ b/camera/camera-camera2-pipe/src/main/java/androidx/camera/camera2/pipe/internal/OutputDistributor.kt
@@ -1,5 +1,5 @@
 /*
- * Copyright 2023 The Android Open Source Project
+ * Copyright 2024 The Android Open Source Project
  *
  * Licensed under the Apache License, Version 2.0 (the "License");
  * you may not use this file except in compliance with the License.
@@ -14,7 +14,7 @@
  * limitations under the License.
  */
 
-package androidx.camera.camera2.pipe.media
+package androidx.camera.camera2.pipe.internal
 
 import android.os.Build
 import androidx.annotation.GuardedBy
@@ -22,6 +22,7 @@
 import androidx.camera.camera2.pipe.CameraTimestamp
 import androidx.camera.camera2.pipe.FrameNumber
 import androidx.camera.camera2.pipe.OutputStatus
+import androidx.camera.camera2.pipe.media.Finalizer
 import kotlinx.atomicfu.atomic
 
 /**
@@ -29,18 +30,18 @@
  *
  * In addition this class must:
  * 1. Track and cancel events due to skipped [onOutputStarted] events.
- * 2. Track and finalize resources due to skipped [onOutputAvailable] events.
+ * 2. Track and finalize resources due to skipped [onOutputResult] events.
  * 3. Track and cancel events that match [onOutputFailure] events.
  * 4. Track and handle out-of-order [onOutputStarted] events.
  * 5. Finalize all resources and cancel all events during [close]
  *
  * This class makes several assumptions:
  * 1. [onOutputStarted] events *usually* arrive in order, relative to each other.
- * 2. [onOutputAvailable] events *usually* arrive in order, relative to each other.
- * 3. [onOutputStarted] events *usually* happen before a corresponding [onOutputAvailable] event
- * 4. [onOutputStarted] events may have a large number of events (1-50) before [onOutputAvailable]
+ * 2. [onOutputResult] events *usually* arrive in order, relative to each other.
+ * 3. [onOutputStarted] events *usually* happen before a corresponding [onOutputResult] event
+ * 4. [onOutputStarted] events may have a large number of events (1-50) before [onOutputResult]
  *      events start coming in.
- * 5. [onOutputStarted] and [onOutputAvailable] are 1:1 under normal circumstances.
+ * 5. [onOutputStarted] and [onOutputResult] are 1:1 under normal circumstances.
  *
  * @param maximumCachedOutputs indicates how many available outputs this distributor will accept
  *   without matching [onOutputStarted] event before closing them with the [outputFinalizer].
@@ -58,16 +59,14 @@
          * once per [OutputDistributor.onOutputStarted] event.
          *
          * On failures (The output being unavailable, the [OutputDistributor] being closed before
-         * an output has arrived, or an explicit output failure event), this method will still be
-         * invoked with a null [output].
+         * an output has arrived, or an explicit output failure event).
          */
         fun onOutputComplete(
             cameraFrameNumber: FrameNumber,
             cameraTimestamp: CameraTimestamp,
             outputSequence: Long,
             outputNumber: Long,
-            outputStatus: OutputStatus,
-            output: T?,
+            outputResult: OutputResult<T>
         )
     }
 
@@ -92,7 +91,7 @@
     private var lastFailedOutputNumber = Long.MIN_VALUE
 
     private val startedOutputs = mutableListOf<StartedOutput<T>>()
-    private val availableOutputs = mutableMapOf<Long, T?>()
+    private val availableOutputs = mutableMapOf<Long, OutputResult<T>>()
 
     /**
      * Indicates a camera2 output has started at a particular frameNumber and timestamp as well as
@@ -102,7 +101,7 @@
      * @param cameraFrameNumber The Camera2 FrameNumber for this output
      * @param cameraTimestamp The Camera2 CameraTimestamp for this output
      * @param outputNumber untyped number that corresponds to the number provided by
-     *   [onOutputAvailable]. For Images, this will likely be the timestamp of the image (Which may
+     *   [onOutputResult]. For Images, this will likely be the timestamp of the image (Which may
      *   be the same as the CameraTimestamp, but may also be different if the timebase of the
      *   the images is different), or the value of the frameNumber if this OutputDistributor is
      *   handling metadata.
@@ -116,10 +115,10 @@
         outputNumber: Long,
         outputListener: OutputListener<T>
     ) {
-        var outputsToCancel: List<StartedOutput<T>>? = null
-        var outputToComplete: T? = null
-        var invokeOutputCompleteListener = false
-        var outputToFinalize: T? = null
+        var missingOutputs: List<StartedOutput<T>>? = null
+        var matchingOutput: OutputResult<T>? = null
+        var invokeOutputListener = false
+        var outputToFinalize: OutputResult<T>? = null
         val isClosed: Boolean
 
         val outputSequence: Long
@@ -131,7 +130,7 @@
                 lastFailedOutputNumber == outputNumber
             ) {
                 outputToFinalize = availableOutputs.remove(outputNumber)
-                invokeOutputCompleteListener = true
+                invokeOutputListener = true
                 return@synchronized
             }
 
@@ -166,9 +165,9 @@
             if (availableOutputs.containsKey(outputNumber)) {
                 // If we found a matching output, get and remove it from the list of
                 // availableOutputs.
-                outputToComplete = availableOutputs.remove(outputNumber)
-                invokeOutputCompleteListener = true
-                outputsToCancel = removeOutputsOlderThan(
+                matchingOutput = availableOutputs.remove(outputNumber)
+                invokeOutputListener = true
+                missingOutputs = removeOutputsOlderThan(
                     isOutOfOrder,
                     outputSequence,
                     outputNumber
@@ -190,49 +189,42 @@
             )
         }
 
-        // Invoke finalizers and listeners outside of the synchronized block to avoid holding locks.
-        outputsToCancel?.let {
-            val reason = if (isClosed) {
-                OutputStatus.ERROR_OUTPUT_ABORTED
-            } else {
-                OutputStatus.ERROR_OUTPUT_MISSING
-            }
-            for (output in it) {
-                output.completeWith(null, reason)
-            }
+        // Handle missing outputs, finalizers, and listeners outside of the synchronized block.
+        missingOutputs?.forEach {
+            it.completeWith(OutputResult.failure(OutputStatus.ERROR_OUTPUT_MISSING))
         }
-        outputToFinalize?.let { outputFinalizer.finalize(it) }
-        if (invokeOutputCompleteListener) {
+        outputToFinalize?.output?.let { outputFinalizer.finalize(it) }
+
+        if (invokeOutputListener) {
             val outputResult = if (isClosed) {
-                OutputStatus.ERROR_OUTPUT_ABORTED
-            } else if (outputToComplete == null) {
-                OutputStatus.ERROR_OUTPUT_DROPPED
+                OutputResult.failure(OutputStatus.ERROR_OUTPUT_ABORTED)
             } else {
-                OutputStatus.AVAILABLE
+                matchingOutput ?: OutputResult.failure(OutputStatus.ERROR_OUTPUT_FAILED)
             }
             outputListener.onOutputComplete(
                 cameraFrameNumber = cameraFrameNumber,
                 cameraTimestamp = cameraTimestamp,
                 outputSequence = outputSequence,
                 outputNumber = outputNumber,
-                outputResult,
-                outputToComplete,
+                outputResult
             )
         }
     }
 
     /**
-     * Indicates a camera2 output has arrived for a specific [outputNumber]. outputNumber will
-     * often refer to a FrameNumber for TotalCaptureResult distribution, and will often refer to a
-     * nanosecond timestamp for ImageReader Image distribution.
+     * Indicates a camera2 output has arrived for a specific [outputNumber].
+     *
+     * This value is the primary keu used to match `onOutputStart` events with `onOutputResult`
+     * events. For images, these values will often refer to the nanosecond timestamp of the Image,
+     * and for TotalCaptureResults, this value will often reference the associated FrameNumber.
      */
-    fun onOutputAvailable(outputNumber: Long, output: T?) {
-        var outputToFinalize: T? = null
+    fun onOutputResult(outputNumber: Long, outputResult: OutputResult<T>) {
+        var outputToFinalize: OutputResult<T>? = null
         var outputsToCancel: List<StartedOutput<T>>? = null
 
         synchronized(lock) {
             if (closed || lastFailedOutputNumber == outputNumber) {
-                outputToFinalize = output
+                outputToFinalize = outputResult
                 return@synchronized
             }
 
@@ -243,18 +235,14 @@
             if (matchingOutput != null) {
                 outputsToCancel = removeOutputsOlderThan(matchingOutput)
 
-                // If the output is null, then we know that the output was intentionally dropped.
-                if (output == null) {
-                    matchingOutput.completeWith(null, OutputStatus.ERROR_OUTPUT_DROPPED)
-                } else {
-                    matchingOutput.completeWith(output, OutputStatus.AVAILABLE)
-                }
+                matchingOutput.completeWith(outputResult)
+
                 startedOutputs.remove(matchingOutput)
                 return@synchronized
             }
 
             // If there is no started output, put this output into the queue of pending outputs.
-            availableOutputs[outputNumber] = output
+            availableOutputs[outputNumber] = outputResult
 
             // If there are too many pending outputs, remove the oldest one.
             if (availableOutputs.size > maximumCachedOutputs) {
@@ -265,8 +253,10 @@
         }
 
         // Invoke finalizers and listeners outside of the synchronized block to avoid holding locks.
-        outputToFinalize?.let { outputFinalizer.finalize(it) }
-        outputsToCancel?.forEach { it.completeWith(null, OutputStatus.ERROR_OUTPUT_MISSING) }
+        outputToFinalize?.output?.let { outputFinalizer.finalize(it) }
+        outputsToCancel?.forEach {
+            it.completeWith(OutputResult.failure(OutputStatus.ERROR_OUTPUT_MISSING))
+        }
     }
 
     /**
@@ -290,7 +280,7 @@
         }
 
         // Invoke listeners outside of the synchronized block to avoid holding locks.
-        outputWithFailure?.completeWith(null, OutputStatus.ERROR_OUTPUT_FAILED)
+        outputWithFailure?.completeWithFailure(OutputStatus.ERROR_OUTPUT_FAILED)
     }
 
     @GuardedBy("lock")
@@ -316,7 +306,7 @@
     }
 
     override fun close() {
-        var outputsToFinalize: List<T?>
+        var outputsToFinalize: List<OutputResult<T>>
         var outputsToCancel: List<StartedOutput<T>>
 
         synchronized(lock) {
@@ -332,10 +322,10 @@
         }
 
         for (pendingOutput in outputsToFinalize) {
-            outputFinalizer.finalize(pendingOutput)
+            outputFinalizer.finalize(pendingOutput.output)
         }
         for (startedOutput in outputsToCancel) {
-            startedOutput.completeWith(null, OutputStatus.ERROR_OUTPUT_ABORTED)
+            startedOutput.completeWithFailure(OutputStatus.ERROR_OUTPUT_ABORTED)
         }
     }
 
@@ -353,7 +343,10 @@
     ) {
         private val complete = atomic(false)
 
-        fun completeWith(output: T?, outputResult: OutputStatus) {
+        fun completeWithFailure(failureReason: OutputStatus) =
+            completeWith(OutputResult.failure(failureReason))
+
+        fun completeWith(outputResult: OutputResult<T>) {
             check(complete.compareAndSet(expect = false, update = true)) {
                 "Output $outputSequence at $cameraFrameNumber for $outputNumber was completed " +
                     "multiple times!"
@@ -363,8 +356,7 @@
                 cameraTimestamp,
                 outputSequence,
                 outputNumber,
-                outputResult,
-                output
+                outputResult
             )
         }
     }
diff --git a/camera/camera-camera2-pipe/src/main/java/androidx/camera/camera2/pipe/internal/OutputResult.kt b/camera/camera-camera2-pipe/src/main/java/androidx/camera/camera2/pipe/internal/OutputResult.kt
new file mode 100644
index 0000000..78e72c6
--- /dev/null
+++ b/camera/camera-camera2-pipe/src/main/java/androidx/camera/camera2/pipe/internal/OutputResult.kt
@@ -0,0 +1,120 @@
+/*
+ * Copyright 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+@file:Suppress("NOTHING_TO_INLINE")
+
+package androidx.camera.camera2.pipe.internal
+
+import androidx.camera.camera2.pipe.OutputStatus
+import kotlinx.coroutines.CompletableDeferred
+import kotlinx.coroutines.Deferred
+import kotlinx.coroutines.ExperimentalCoroutinesApi
+
+/**
+ * Inline value class that can be used in place of `Any` to represent either a valid output object
+ * OR an [OutputStatus] indicating why the result is not available.
+ */
+@JvmInline
+internal value class OutputResult<out T> private constructor(internal val result: Any?) {
+    /**
+     * Returns `true` if this instance represents a successful outcome.
+     */
+    val available: Boolean get() = !failure && result != null
+
+    /**
+     * Returns `true` if this instance represents a failed result.
+     */
+    val failure: Boolean get() = result is OutputStatus
+
+    /**
+     * Returns the value, if [from], else null.
+     */
+    @Suppress("UNCHECKED_CAST")
+    inline val output: T?
+        get() =
+            when {
+                available -> result as T
+                else -> null
+            }
+
+    /**
+     * If this OutputResult represents a failure, then return the [OutputStatus] associated with it,
+     * otherwise report [OutputStatus.AVAILABLE] for successfully cases.
+     */
+    inline val status: OutputStatus
+        get() =
+            when {
+                available -> OutputStatus.AVAILABLE
+                result == null -> OutputStatus.UNAVAILABLE
+                else -> result as OutputStatus
+            }
+
+    @OptIn(ExperimentalCoroutinesApi::class)
+    companion object {
+        /**
+         * Returns an instance that encapsulates the given [output] as successful value.
+         */
+        inline fun <T> from(output: T): OutputResult<T> =
+            OutputResult(output as Any?)
+
+        /**
+         * Returns an instance that encapsulates the given OutputStatus as a failure.
+         */
+        inline fun <T> failure(failureReason: OutputStatus): OutputResult<T> =
+            OutputResult(failureReason)
+
+        /** Utility function to complete a CompletableDeferred with a successful [OutputResult]. */
+        inline fun <T> CompletableDeferred<OutputResult<T>>.completeWithOutput(output: T): Boolean {
+            return complete(from(output))
+        }
+
+        /** Utility function to complete a CompletableDeferred with a [OutputStatus] failure */
+        inline fun <T> CompletableDeferred<OutputResult<T>>.completeWithFailure(
+            status: OutputStatus
+        ): Boolean {
+            return complete(failure(status))
+        }
+
+        /**
+         * For a [Deferred] object that contains an OutputResult, determine the status based on the
+         * state of the [Deferred] or from the actual object if this status has been completed.
+         */
+        inline fun <T> Deferred<OutputResult<T>>.outputStatus(): OutputStatus {
+            return if (!isCompleted) {
+                // If the result is not completed, then this Output is in a PENDING state.
+                OutputStatus.PENDING
+            } else if (isCancelled) {
+                // If the result was canceled for any reason, then this Output is, and will not, be
+                // available.
+                OutputStatus.UNAVAILABLE
+            } else {
+                // If we reach here, the result is A) completed, and B) not canceled. read the
+                // status from the result and return it.
+                getCompleted().status
+            }
+        }
+
+        /**
+         * Get the output from this [Deferred], if available, or null.
+         */
+        inline fun <T> Deferred<OutputResult<T>>.outputOrNull(): T? {
+            if (isCompleted && !isCancelled) {
+                return getCompleted().output
+            }
+            return null
+        }
+    }
+}
diff --git a/camera/camera-camera2-pipe/src/test/java/androidx/camera/camera2/pipe/internal/FrameCaptureQueueTest.kt b/camera/camera-camera2-pipe/src/test/java/androidx/camera/camera2/pipe/internal/FrameCaptureQueueTest.kt
new file mode 100644
index 0000000..7362c7f
--- /dev/null
+++ b/camera/camera-camera2-pipe/src/test/java/androidx/camera/camera2/pipe/internal/FrameCaptureQueueTest.kt
@@ -0,0 +1,141 @@
+/*
+ * Copyright 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR 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.internal
+
+import android.os.Build
+import androidx.camera.camera2.pipe.OutputStatus
+import androidx.camera.camera2.pipe.Request
+import androidx.camera.camera2.pipe.StreamId
+import com.google.common.truth.Truth.assertThat
+import org.junit.Test
+import org.junit.runner.RunWith
+import org.robolectric.RobolectricTestRunner
+import org.robolectric.annotation.Config
+
+/** Tests for [FrameCaptureQueue] */
+@RunWith(RobolectricTestRunner::class)
+@Config(minSdk = Build.VERSION_CODES.LOLLIPOP)
+class FrameCaptureQueueTest {
+    private val imageStreams = listOf(StreamId(1), StreamId(2), StreamId(3))
+    private val captureQueue = FrameCaptureQueue()
+
+    private val request1 = Request(
+        streams = imageStreams
+    )
+    private val request2 = Request(
+        streams = imageStreams
+    )
+    private val request3 = Request(
+        streams = imageStreams
+    )
+
+    @Test
+    fun outputFrameQueueHoldsDeferredFrames() {
+        val frameCapture = captureQueue.enqueue(request1)
+        assertThat(frameCapture.status).isEqualTo(OutputStatus.PENDING)
+    }
+
+    @Test
+    fun outputFrameQueueCanRemoveDeferredFrames() {
+        val deferred = captureQueue.enqueue(request1)
+
+        val deferred1 = captureQueue.remove(request1)
+        val deferred2 = captureQueue.remove(request1)
+        assertThat(deferred1).isSameInstanceAs(deferred)
+        assertThat(deferred2).isNull()
+    }
+
+    @Test
+    fun outputFrameQueueHoldsMultipleDeferredFramesInOrder() {
+        val deferred1 = captureQueue.enqueue(request1)
+        val deferred2 = captureQueue.enqueue(request1)
+        val deferred3 = captureQueue.enqueue(request1)
+
+        assertThat(deferred1).isNotSameInstanceAs(deferred2)
+        assertThat(deferred2).isNotSameInstanceAs(deferred3)
+
+        val removedFrame1 = captureQueue.remove(request1)
+        val removedFrame2 = captureQueue.remove(request1)
+        val removedFrame3 = captureQueue.remove(request1)
+        val removedFrame4 = captureQueue.remove(request1)
+
+        assertThat(removedFrame1).isSameInstanceAs(deferred1)
+        assertThat(removedFrame2).isSameInstanceAs(deferred2)
+        assertThat(removedFrame3).isSameInstanceAs(deferred3)
+        assertThat(removedFrame4).isNull()
+    }
+
+    @Test
+    fun outputFrameQueueCanRemoveIntermixedRequests() {
+        // Intermixed requests (2 request1's, 1 request2, 1 request3)
+        val frame1 = captureQueue.enqueue(request1)
+        val frame2 = captureQueue.enqueue(request2)
+        val frame3 = captureQueue.enqueue(request1)
+        val frame4 = captureQueue.enqueue(request3)
+
+        // Remove request1's first
+        val removedFrame1 = captureQueue.remove(request1)
+        val removedFrame2 = captureQueue.remove(request1)
+        val removedFrame3 = captureQueue.remove(request3)
+        val removedFrame4 = captureQueue.remove(request2)
+
+        assertThat(removedFrame1).isSameInstanceAs(frame1)
+        assertThat(removedFrame2).isSameInstanceAs(frame3)
+        assertThat(removedFrame3).isSameInstanceAs(frame4)
+        assertThat(removedFrame4).isSameInstanceAs(frame2)
+    }
+
+    @Test
+    fun closingOutputFrameRemovesItFromOutputFrameQueue() {
+        val frame1 = captureQueue.enqueue(request1)
+        val frame2 = captureQueue.enqueue(request1)
+        val frame3 = captureQueue.enqueue(request1)
+
+        frame2.close()
+
+        val removedFrame1 = captureQueue.remove(request1)
+        val removedFrame2 = captureQueue.remove(request1)
+        val removedFrame3 = captureQueue.remove(request1)
+
+        assertThat(removedFrame1).isSameInstanceAs(frame1)
+        assertThat(removedFrame2).isSameInstanceAs(frame3)
+        assertThat(removedFrame3).isNull()
+    }
+
+    @Test
+    fun closingDeferredFrameCancelsResults() {
+        val frame = captureQueue.enqueue(request1)
+        frame.close()
+
+        val frameFromQueue = captureQueue.remove(request1)
+        assertThat(frameFromQueue).isNull()
+    }
+
+    @Test
+    fun canEnqueueMultipleResults() {
+        val frames = captureQueue.enqueue(listOf(request1, request1, request1))
+        assertThat(frames.size).isEqualTo(3)
+
+        val removedFrame1 = captureQueue.remove(request1)
+        val removedFrame2 = captureQueue.remove(request1)
+        val removedFrame3 = captureQueue.remove(request1)
+
+        assertThat(removedFrame1).isSameInstanceAs(frames[0])
+        assertThat(removedFrame2).isSameInstanceAs(frames[1])
+        assertThat(removedFrame3).isSameInstanceAs(frames[2])
+    }
+}
diff --git a/camera/camera-camera2-pipe/src/test/java/androidx/camera/camera2/pipe/internal/FrameDistributorTest.kt b/camera/camera-camera2-pipe/src/test/java/androidx/camera/camera2/pipe/internal/FrameDistributorTest.kt
new file mode 100644
index 0000000..1ff606c
--- /dev/null
+++ b/camera/camera-camera2-pipe/src/test/java/androidx/camera/camera2/pipe/internal/FrameDistributorTest.kt
@@ -0,0 +1,290 @@
+/*
+ * Copyright 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR 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.internal
+
+import android.os.Build
+import android.util.Size
+import androidx.camera.camera2.pipe.CameraStream
+import androidx.camera.camera2.pipe.CameraTimestamp
+import androidx.camera.camera2.pipe.Frame
+import androidx.camera.camera2.pipe.Frame.Companion.isFrameInfoAvailable
+import androidx.camera.camera2.pipe.Frame.Companion.isImageAvailable
+import androidx.camera.camera2.pipe.FrameCapture
+import androidx.camera.camera2.pipe.FrameNumber
+import androidx.camera.camera2.pipe.FrameReference
+import androidx.camera.camera2.pipe.FrameReference.Companion.acquire
+import androidx.camera.camera2.pipe.OutputStatus
+import androidx.camera.camera2.pipe.Request
+import androidx.camera.camera2.pipe.StreamFormat
+import androidx.camera.camera2.pipe.testing.FakeFrameInfo
+import androidx.camera.camera2.pipe.testing.FakeFrameMetadata
+import androidx.camera.camera2.pipe.testing.FakeRequestFailure
+import androidx.camera.camera2.pipe.testing.FakeRequestMetadata
+import androidx.camera.camera2.pipe.testing.ImageSimulator
+import com.google.common.truth.Truth.assertThat
+import org.junit.After
+import org.junit.Test
+import org.junit.runner.RunWith
+import org.robolectric.RobolectricTestRunner
+import org.robolectric.annotation.Config
+
+/** Tests for [FrameDistributor] */
+@RunWith(RobolectricTestRunner::class)
+@Config(minSdk = Build.VERSION_CODES.LOLLIPOP)
+class FrameDistributorTest {
+
+    private val stream1Config =
+        CameraStream.Config.create(
+            Size(1280, 720),
+            StreamFormat.YUV_420_888
+        )
+    private val stream2Config =
+        CameraStream.Config.create(
+            Size(1920, 1080), StreamFormat.YUV_420_888
+        )
+    private val streamConfigs = listOf(stream1Config, stream2Config)
+
+    private val imageSimulator = ImageSimulator(streamConfigs)
+    private val stream1Id = imageSimulator.streamGraph[stream1Config]!!.id
+    private val stream2Id = imageSimulator.streamGraph[stream2Config]!!.id
+    private val streams = listOf(stream1Id, stream2Id)
+
+    private val cameraId = imageSimulator.cameraMetadata.camera
+    private val cameraTimestamp = CameraTimestamp(1234L)
+    private val cameraFrameNumber = FrameNumber(420)
+
+    private val request = Request(streams = streams)
+    private val fakeRequestMetadata = FakeRequestMetadata.from(
+        request,
+        imageSimulator.streamToSurfaceMap,
+        repeating = false
+    )
+    private val fakeFrameInfo = FakeFrameInfo(
+        metadata = FakeFrameMetadata(
+            camera = cameraId,
+            frameNumber = cameraFrameNumber
+        ),
+        requestMetadata = fakeRequestMetadata
+    )
+
+    private val fakeFrameBuffer = FakeFrameBuffer()
+    private val frameCaptureQueue = FrameCaptureQueue()
+    private val frameDistributor =
+        FrameDistributor(imageSimulator.imageSources, frameCaptureQueue, fakeFrameBuffer)
+
+    @Test
+    fun frameDistributorSetupVerification() {
+        assertThat(imageSimulator.imageSources.keys).containsExactly(stream1Id, stream2Id)
+        assertThat(imageSimulator.streamToSurfaceMap.keys).containsExactly(stream1Id, stream2Id)
+    }
+
+    @Test
+    fun framesAreAddedToFrameBuffer() {
+        frameDistributor.onStarted(
+            fakeRequestMetadata,
+            cameraFrameNumber,
+            cameraTimestamp
+        )
+
+        assertThat(fakeFrameBuffer.frames.size).isEqualTo(1)
+
+        val frame = fakeFrameBuffer.frames[0]
+        assertThat(frame.frameInfoStatus).isEqualTo(OutputStatus.PENDING)
+        assertThat(frame.imageStatus(stream1Id)).isEqualTo(OutputStatus.PENDING)
+        assertThat(frame.imageStatus(stream2Id)).isEqualTo(OutputStatus.PENDING)
+        assertThat(frame.imageStreams).containsExactly(stream1Id, stream2Id)
+
+        // Closing should cause all outputs to be closed, since this should be the only frame.
+        frame.close()
+
+        assertThat(frame.imageStreams).containsExactly(stream1Id, stream2Id)
+
+        assertThat(frame.getFrameInfo()).isEqualTo(null)
+        assertThat(frame.getImage(stream1Id)).isEqualTo(null)
+        assertThat(frame.getImage(stream2Id)).isEqualTo(null)
+
+        assertThat(frame.frameInfoStatus).isEqualTo(OutputStatus.UNAVAILABLE)
+        assertThat(frame.imageStatus(stream1Id)).isEqualTo(OutputStatus.UNAVAILABLE)
+        assertThat(frame.imageStatus(stream2Id)).isEqualTo(OutputStatus.UNAVAILABLE)
+    }
+
+    @Test
+    fun outputsAreDistributedToFrame() {
+        frameDistributor.onStarted(
+            fakeRequestMetadata,
+            cameraFrameNumber,
+            cameraTimestamp
+        )
+
+        assertThat(fakeFrameBuffer.frames.size).isEqualTo(1)
+        val frame = fakeFrameBuffer.frames[0]
+
+        val image1 = imageSimulator.simulateImage(stream1Id, cameraTimestamp.value)
+        assertThat(frame.isImageAvailable(stream1Id)).isTrue()
+        assertThat(frame.isImageAvailable(stream2Id)).isFalse()
+        assertThat(frame.isFrameInfoAvailable).isFalse()
+        assertThat(image1.isClosed).isFalse()
+
+        val image2 = imageSimulator.simulateImage(stream2Id, cameraTimestamp.value)
+        assertThat(frame.isImageAvailable(stream2Id)).isTrue()
+        assertThat(frame.isFrameInfoAvailable).isFalse()
+        assertThat(image2.isClosed).isFalse()
+
+        frameDistributor.onComplete(
+            fakeRequestMetadata,
+            cameraFrameNumber,
+            fakeFrameInfo
+        )
+        assertThat(frame.isFrameInfoAvailable).isTrue()
+
+        // Now close the frame (without acquiring images)
+        frame.close()
+
+        // Assert that the images are closed
+        assertThat(image1.isClosed).isTrue()
+        assertThat(image2.isClosed).isTrue()
+    }
+
+    @Test
+    fun onStartedCausesFrameCaptureToBeAvailable() {
+        val frameCapture = frameCaptureQueue.enqueue(fakeRequestMetadata.request) as FrameCapture
+        assertThat(frameCapture.status).isEqualTo(OutputStatus.PENDING)
+
+        frameDistributor.onStarted(
+            fakeRequestMetadata,
+            cameraFrameNumber,
+            cameraTimestamp
+        )
+
+        assertThat(frameCapture.status).isEqualTo(OutputStatus.AVAILABLE)
+        val frame = frameCapture.getFrame()
+        assertThat(frame).isNotNull()
+        frame?.close()
+    }
+
+    @Test
+    fun abortedRequestsCauseFramesToBeAborted() {
+        val frameCapture = frameCaptureQueue.enqueue(fakeRequestMetadata.request)
+        frameDistributor.onAborted(
+            fakeRequestMetadata.request
+        )
+        assertThat(frameCapture.status).isEqualTo(OutputStatus.ERROR_OUTPUT_ABORTED)
+        assertThat(frameCapture.getFrame()).isNull()
+    }
+
+    @Test
+    fun onFailureCausesFrameInfoToBeLost() {
+        val frameCapture = frameCaptureQueue.enqueue(fakeRequestMetadata.request)
+        frameDistributor.onStarted(
+            fakeRequestMetadata,
+            cameraFrameNumber,
+            cameraTimestamp
+        )
+        val frame = frameCapture.getFrame()!!
+
+        assertThat(frame.frameInfoStatus).isEqualTo(OutputStatus.PENDING)
+        assertThat(frame.imageStatus(stream1Id)).isEqualTo(OutputStatus.PENDING)
+        assertThat(frame.imageStatus(stream2Id)).isEqualTo(OutputStatus.PENDING)
+
+        frameDistributor.onFailed(
+            fakeRequestMetadata,
+            cameraFrameNumber,
+            FakeRequestFailure(fakeRequestMetadata, cameraFrameNumber, wasImageCaptured = true)
+        )
+
+        assertThat(frame.frameInfoStatus).isEqualTo(OutputStatus.ERROR_OUTPUT_FAILED)
+        assertThat(frame.imageStatus(stream1Id)).isEqualTo(OutputStatus.PENDING)
+        assertThat(frame.imageStatus(stream2Id)).isEqualTo(OutputStatus.PENDING)
+
+        // Images are still delivered, even after onFailed
+        imageSimulator.simulateImage(stream1Id, cameraTimestamp.value)
+        imageSimulator.simulateImage(stream2Id, cameraTimestamp.value)
+
+        assertThat(frame.frameInfoStatus).isEqualTo(OutputStatus.ERROR_OUTPUT_FAILED)
+        assertThat(frame.isImageAvailable(stream1Id)).isEqualTo(true)
+        assertThat(frame.isImageAvailable(stream2Id)).isEqualTo(true)
+    }
+
+    @Test
+    fun onFailureWithImageLossAllOutputsToFail() {
+        val frameCapture = frameCaptureQueue.enqueue(fakeRequestMetadata.request)
+        frameDistributor.onStarted(
+            fakeRequestMetadata,
+            cameraFrameNumber,
+            cameraTimestamp
+        )
+        val frame = frameCapture.getFrame()!!
+
+        assertThat(frame.frameInfoStatus).isEqualTo(OutputStatus.PENDING)
+        assertThat(frame.imageStatus(stream1Id)).isEqualTo(OutputStatus.PENDING)
+        assertThat(frame.imageStatus(stream2Id)).isEqualTo(OutputStatus.PENDING)
+
+        frameDistributor.onFailed(
+            fakeRequestMetadata,
+            cameraFrameNumber,
+            FakeRequestFailure(fakeRequestMetadata, cameraFrameNumber, wasImageCaptured = false)
+        )
+
+        assertThat(frame.frameInfoStatus).isEqualTo(OutputStatus.ERROR_OUTPUT_FAILED)
+        assertThat(frame.imageStatus(stream1Id)).isEqualTo(OutputStatus.ERROR_OUTPUT_FAILED)
+        assertThat(frame.imageStatus(stream2Id)).isEqualTo(OutputStatus.ERROR_OUTPUT_FAILED)
+
+        // Images are still delivered, even after onFailed
+        val fakeImage1 = imageSimulator.simulateImage(stream1Id, cameraTimestamp.value)
+        val fakeImage2 = imageSimulator.simulateImage(stream2Id, cameraTimestamp.value)
+
+        assertThat(fakeImage1.isClosed).isTrue()
+        assertThat(fakeImage2.isClosed).isTrue()
+    }
+
+    @After
+    fun cleanup() {
+        imageSimulator.close()
+    }
+
+    private class FakeFrameBuffer : FrameDistributor.FrameStartedListener,
+        AutoCloseable {
+        private val lock = Any()
+        private var closed = false
+        private val _frames = mutableListOf<Frame>()
+        val frames: List<Frame>
+            get() = synchronized(lock) { _frames.toList() }
+
+        override fun onFrameStarted(frameReference: FrameReference) {
+            synchronized(lock) {
+                if (!closed) {
+                    _frames.add(frameReference.acquire())
+                }
+            }
+        }
+
+        override fun close() {
+            val shouldClose: Boolean
+            synchronized(lock) {
+                shouldClose = !closed
+                closed = true
+            }
+
+            if (shouldClose) {
+                for (outputFrame in _frames) {
+                    outputFrame.close()
+                }
+                _frames.clear()
+            }
+        }
+    }
+}
diff --git a/camera/camera-camera2-pipe/src/test/java/androidx/camera/camera2/pipe/internal/FrameImplTest.kt b/camera/camera-camera2-pipe/src/test/java/androidx/camera/camera2/pipe/internal/FrameImplTest.kt
index f157e96..cccea05 100644
--- a/camera/camera-camera2-pipe/src/test/java/androidx/camera/camera2/pipe/internal/FrameImplTest.kt
+++ b/camera/camera-camera2-pipe/src/test/java/androidx/camera/camera2/pipe/internal/FrameImplTest.kt
@@ -21,7 +21,6 @@
 import androidx.camera.camera2.pipe.CameraTimestamp
 import androidx.camera.camera2.pipe.Frame.Companion.isFrameInfoAvailable
 import androidx.camera.camera2.pipe.Frame.Companion.isImageAvailable
-import androidx.camera.camera2.pipe.FrameId
 import androidx.camera.camera2.pipe.FrameNumber
 import androidx.camera.camera2.pipe.OutputId
 import androidx.camera.camera2.pipe.OutputStatus
@@ -57,7 +56,6 @@
     private val stream2Surface = fakeSurfaces.createFakeSurface(Size(640, 480))
     private val streamToSurfaceMap = mapOf(stream1Id to stream1Surface, stream2Id to stream2Surface)
 
-    private val frameId = FrameId(240)
     private val frameNumber = FrameNumber(420)
     private val frameTimestampNs = 1234L
     private val frameTimestamp = CameraTimestamp(frameTimestampNs)
@@ -72,7 +70,6 @@
 
     private val frameState = FrameState(
         requestMetadata = fakeRequestMetadata,
-        frameId = frameId,
         frameNumber = frameNumber,
         frameTimestamp = frameTimestamp,
         imageStreams
@@ -311,8 +308,7 @@
             frameTimestamp,
             42,
             frameTimestamp.value,
-            OutputStatus.ERROR_OUTPUT_DROPPED,
-            null
+            OutputResult.failure(OutputStatus.ERROR_OUTPUT_DROPPED)
         )
 
         assertThat(sharedOutputFrame.imageStatus(stream1Id))
@@ -338,8 +334,7 @@
             frameTimestamp,
             42,
             frameTimestamp.value,
-            OutputStatus.AVAILABLE,
-            stream1OutputImage
+            OutputResult.from(stream1OutputImage)
         )
 
         // Complete streamResult2 with stream2Output3Image
@@ -348,8 +343,7 @@
             frameTimestamp,
             42,
             frameTimestamp.value,
-            OutputStatus.AVAILABLE,
-            stream2OutputImage
+            OutputResult.from(stream2OutputImage)
         )
 
         // Complete frameInfoResult
@@ -358,8 +352,7 @@
             frameTimestamp,
             42,
             frameNumber.value,
-            OutputStatus.AVAILABLE,
-            fakeFrameInfo
+            OutputResult.from(fakeFrameInfo)
         )
     }
 }
diff --git a/camera/camera-camera2-pipe/src/test/java/androidx/camera/camera2/pipe/internal/FrameStateTest.kt b/camera/camera-camera2-pipe/src/test/java/androidx/camera/camera2/pipe/internal/FrameStateTest.kt
index 6d28f9e..77e6629 100644
--- a/camera/camera-camera2-pipe/src/test/java/androidx/camera/camera2/pipe/internal/FrameStateTest.kt
+++ b/camera/camera-camera2-pipe/src/test/java/androidx/camera/camera2/pipe/internal/FrameStateTest.kt
@@ -18,7 +18,6 @@
 
 import android.os.Build
 import androidx.camera.camera2.pipe.CameraTimestamp
-import androidx.camera.camera2.pipe.FrameId
 import androidx.camera.camera2.pipe.FrameNumber
 import androidx.camera.camera2.pipe.OutputId
 import androidx.camera.camera2.pipe.OutputStatus
@@ -46,7 +45,6 @@
 
     private val output1Id = OutputId(1)
 
-    private val frameId = FrameId(240)
     private val frameNumber = FrameNumber(420)
     private val frameTimestampNs = 1234L
     private val frameTimestamp = CameraTimestamp(frameTimestampNs)
@@ -75,7 +73,6 @@
 
     private val frameState = FrameState(
         requestMetadata = fakeRequestMetadata,
-        frameId = frameId,
         frameNumber = frameNumber,
         frameTimestamp = frameTimestamp,
         imageStreams
@@ -117,8 +114,7 @@
             frameTimestamp,
             64L,
             frameTimestampNs,
-            OutputStatus.AVAILABLE,
-            outputImage
+            OutputResult.from(outputImage)
         )
 
         assertThat(fakeImage.isClosed).isFalse()
@@ -129,7 +125,7 @@
         assertThat(fakeImage.isClosed).isTrue()
         assertThat(imageResult1.status).isEqualTo(OutputStatus.UNAVAILABLE)
 
-        val result = imageResult1.getCompletedOrNull()
+        val result = imageResult1.outputOrNull()
         assertThat(result).isNull()
     }
 
@@ -142,12 +138,11 @@
             frameTimestamp,
             64L,
             frameTimestampNs,
-            OutputStatus.AVAILABLE,
-            outputImage
+            OutputResult.from(outputImage)
         )
 
         assertThat(fakeImage.isClosed).isTrue()
-        assertThat(imageResult1.getCompletedOrNull()).isNull()
+        assertThat(imageResult1.outputOrNull()).isNull()
     }
 
     @Test
@@ -157,11 +152,10 @@
             frameTimestamp,
             64L,
             frameTimestampNs,
-            OutputStatus.AVAILABLE,
-            outputImage
+            OutputResult.from(outputImage)
         )
-        val imageCopy1 = imageResult1.getCompletedOrNull()
-        val imageCopy2 = imageResult1.getCompletedOrNull()
+        val imageCopy1 = imageResult1.outputOrNull()
+        val imageCopy2 = imageResult1.outputOrNull()
 
         assertThat(imageCopy1).isNotNull()
         assertThat(imageCopy2).isNotNull()
@@ -185,12 +179,11 @@
             frameTimestamp,
             10,
             frameNumber.value,
-            OutputStatus.AVAILABLE,
-            fakeFrameInfo
+            OutputResult.from(fakeFrameInfo)
         )
 
         assertThat(frameState.frameInfoOutput.status).isEqualTo(OutputStatus.AVAILABLE)
-        assertThat(frameState.frameInfoOutput.getCompletedOrNull()).isSameInstanceAs(fakeFrameInfo)
+        assertThat(frameState.frameInfoOutput.outputOrNull()).isSameInstanceAs(fakeFrameInfo)
     }
 
     @Test
@@ -201,11 +194,10 @@
             frameTimestamp,
             10,
             frameNumber.value,
-            OutputStatus.AVAILABLE,
-            fakeFrameInfo
+            OutputResult.from(fakeFrameInfo)
         )
 
         assertThat(frameState.frameInfoOutput.status).isEqualTo(OutputStatus.UNAVAILABLE)
-        assertThat(frameState.frameInfoOutput.getCompletedOrNull()).isNull()
+        assertThat(frameState.frameInfoOutput.outputOrNull()).isNull()
     }
 }
diff --git a/camera/camera-camera2-pipe/src/test/java/androidx/camera/camera2/pipe/media/OutputDistributorTest.kt b/camera/camera-camera2-pipe/src/test/java/androidx/camera/camera2/pipe/internal/OutputDistributorTest.kt
similarity index 81%
rename from camera/camera-camera2-pipe/src/test/java/androidx/camera/camera2/pipe/media/OutputDistributorTest.kt
rename to camera/camera-camera2-pipe/src/test/java/androidx/camera/camera2/pipe/internal/OutputDistributorTest.kt
index af11146..42bf6cc 100644
--- a/camera/camera-camera2-pipe/src/test/java/androidx/camera/camera2/pipe/media/OutputDistributorTest.kt
+++ b/camera/camera-camera2-pipe/src/test/java/androidx/camera/camera2/pipe/internal/OutputDistributorTest.kt
@@ -1,5 +1,5 @@
 /*
- * Copyright 2023 The Android Open Source Project
+ * Copyright 2024 The Android Open Source Project
  *
  * Licensed under the Apache License, Version 2.0 (the "License");
  * you may not use this file except in compliance with the License.
@@ -14,13 +14,14 @@
  * limitations under the License.
  */
 
-package androidx.camera.camera2.pipe.media
+package androidx.camera.camera2.pipe.internal
 
 import android.os.Build
 import androidx.camera.camera2.pipe.CameraTimestamp
 import androidx.camera.camera2.pipe.FrameNumber
 import androidx.camera.camera2.pipe.OutputStatus
-import androidx.camera.camera2.pipe.media.OutputDistributor.OutputListener
+import androidx.camera.camera2.pipe.internal.OutputDistributor.OutputListener
+import androidx.camera.camera2.pipe.media.Finalizer
 import com.google.common.truth.Truth.assertThat
 import kotlinx.atomicfu.atomic
 import org.junit.Test
@@ -61,7 +62,7 @@
 
     @Test
     fun onOutputAvailableDoesNotFinalizeOutputs() {
-        outputDistributor.onOutputAvailable(fakeOutput1.outputNumber, fakeOutput1)
+        outputDistributor.onOutputResult(fakeOutput1.outputNumber, OutputResult.from(fakeOutput1))
 
         // When an output becomes available, ensure it is not immediately finalized.
         assertThat(fakeOutput1.finalized).isFalse()
@@ -69,10 +70,10 @@
 
     @Test
     fun onOutputAvailableEvictsAndFinalizesPreviousOutputs() {
-        outputDistributor.onOutputAvailable(fakeOutput1.outputNumber, fakeOutput1)
-        outputDistributor.onOutputAvailable(fakeOutput2.outputNumber, fakeOutput2)
-        outputDistributor.onOutputAvailable(fakeOutput3.outputNumber, fakeOutput3)
-        outputDistributor.onOutputAvailable(fakeOutput4.outputNumber, fakeOutput4)
+        outputDistributor.onOutputResult(fakeOutput1.outputNumber, OutputResult.from(fakeOutput1))
+        outputDistributor.onOutputResult(fakeOutput2.outputNumber, OutputResult.from(fakeOutput2))
+        outputDistributor.onOutputResult(fakeOutput3.outputNumber, OutputResult.from(fakeOutput3))
+        outputDistributor.onOutputResult(fakeOutput4.outputNumber, OutputResult.from(fakeOutput4))
 
         // outputDistributor will only cache up to three outputs without matching start events.
 
@@ -87,10 +88,13 @@
 
     @Test
     fun onOutputAvailableEvictsAndFinalizesOutputsInSequence() {
-        outputDistributor.onOutputAvailable(fakeOutput2.outputNumber, fakeOutput2)
-        outputDistributor.onOutputAvailable(fakeOutput3.outputNumber, fakeOutput3)
-        outputDistributor.onOutputAvailable(fakeOutput4.outputNumber, fakeOutput4)
-        outputDistributor.onOutputAvailable(fakeOutput1.outputNumber, fakeOutput1) // Out of order
+        outputDistributor.onOutputResult(fakeOutput2.outputNumber, OutputResult.from(fakeOutput2))
+        outputDistributor.onOutputResult(fakeOutput3.outputNumber, OutputResult.from(fakeOutput3))
+        outputDistributor.onOutputResult(fakeOutput4.outputNumber, OutputResult.from(fakeOutput4))
+        outputDistributor.onOutputResult(
+            fakeOutput1.outputNumber,
+            OutputResult.from(fakeOutput1)
+        ) // Out of order
 
         // FIFO Order for outputs, regardless of the output number.
         // Note: Outputs are provided as [2, 3, 4, *1*]
@@ -102,13 +106,22 @@
 
     @Test
     fun onOutputAvailableWithNullEvictsAndFinalizesOutputs() {
-        outputDistributor.onOutputAvailable(fakeOutput1.outputNumber, fakeOutput1)
-        outputDistributor.onOutputAvailable(fakeOutput2.outputNumber, fakeOutput2)
-        outputDistributor.onOutputAvailable(fakeOutput3.outputNumber, fakeOutput3)
+        outputDistributor.onOutputResult(fakeOutput1.outputNumber, OutputResult.from(fakeOutput1))
+        outputDistributor.onOutputResult(fakeOutput2.outputNumber, OutputResult.from(fakeOutput2))
+        outputDistributor.onOutputResult(fakeOutput3.outputNumber, OutputResult.from(fakeOutput3))
 
-        outputDistributor.onOutputAvailable(fakeOutput4.outputNumber, null)
-        outputDistributor.onOutputAvailable(fakeOutput5.outputNumber, null)
-        outputDistributor.onOutputAvailable(fakeOutput6.outputNumber, null)
+        outputDistributor.onOutputResult(
+            fakeOutput4.outputNumber,
+            OutputResult.failure(OutputStatus.ERROR_OUTPUT_DROPPED)
+        )
+        outputDistributor.onOutputResult(
+            fakeOutput5.outputNumber,
+            OutputResult.failure(OutputStatus.ERROR_OUTPUT_DROPPED)
+        )
+        outputDistributor.onOutputResult(
+            fakeOutput6.outputNumber,
+            OutputResult.failure(OutputStatus.ERROR_OUTPUT_DROPPED)
+        )
 
         // Dropped outputs (null) still evict old outputs.
         assertThat(fakeOutput1.finalized).isTrue()
@@ -118,8 +131,8 @@
 
     @Test
     fun closingOutputDistributorFinalizesCachedOutputs() {
-        outputDistributor.onOutputAvailable(fakeOutput1.outputNumber, fakeOutput1)
-        outputDistributor.onOutputAvailable(fakeOutput2.outputNumber, fakeOutput2)
+        outputDistributor.onOutputResult(fakeOutput1.outputNumber, OutputResult.from(fakeOutput1))
+        outputDistributor.onOutputResult(fakeOutput2.outputNumber, OutputResult.from(fakeOutput2))
 
         // Outputs that have not been matched with started events must be closed when the
         // outputDistributor is closed.
@@ -134,8 +147,8 @@
         outputDistributor.close()
 
         // Outputs that occur after close must always be finalized immediately.
-        outputDistributor.onOutputAvailable(fakeOutput1.outputNumber, fakeOutput1)
-        outputDistributor.onOutputAvailable(fakeOutput2.outputNumber, fakeOutput2)
+        outputDistributor.onOutputResult(fakeOutput1.outputNumber, OutputResult.from(fakeOutput1))
+        outputDistributor.onOutputResult(fakeOutput2.outputNumber, OutputResult.from(fakeOutput2))
 
         assertThat(fakeOutput1.finalized).isTrue()
         assertThat(fakeOutput2.finalized).isTrue()
@@ -146,7 +159,7 @@
         // When a a start event occurs and an output is also available, ensure the callback
         // is correctly invoked.
         outputDistributor.startWith(pendingOutput1)
-        outputDistributor.onOutputAvailable(fakeOutput1.outputNumber, fakeOutput1)
+        outputDistributor.onOutputResult(fakeOutput1.outputNumber, OutputResult.from(fakeOutput1))
 
         assertThat(pendingOutput1.isComplete).isTrue()
         assertThat(pendingOutput1.output).isEqualTo(fakeOutput1)
@@ -165,7 +178,10 @@
     @Test
     fun pendingResultsAreMatchedWithNullOutputs() {
         outputDistributor.startWith(pendingOutput1)
-        outputDistributor.onOutputAvailable(fakeOutput1.outputNumber, null)
+        outputDistributor.onOutputResult(
+            fakeOutput1.outputNumber,
+            OutputResult.failure(OutputStatus.ERROR_OUTPUT_DROPPED)
+        )
 
         assertThat(pendingOutput1.isComplete).isTrue()
         assertThat(pendingOutput1.output).isNull()
@@ -179,7 +195,10 @@
         outputDistributor.startWith(pendingOutput3)
         outputDistributor.startWith(pendingOutput4)
 
-        outputDistributor.onOutputAvailable(fakeOutput3.outputNumber, fakeOutput3) // Match 3
+        outputDistributor.onOutputResult(
+            fakeOutput3.outputNumber,
+            OutputResult.from(fakeOutput3)
+        ) // Match 3
 
         assertThat(pendingOutput1.isComplete).isTrue() // #1 is Canceled
         assertThat(pendingOutput2.isComplete).isTrue() // #2 is Canceled
@@ -231,8 +250,8 @@
 
     @Test
     fun availableOutputsAreNotDistributedToStartedOutputsAfterClose() {
-        outputDistributor.onOutputAvailable(fakeOutput1.outputNumber, fakeOutput1)
-        outputDistributor.onOutputAvailable(fakeOutput2.outputNumber, fakeOutput2)
+        outputDistributor.onOutputResult(fakeOutput1.outputNumber, OutputResult.from(fakeOutput1))
+        outputDistributor.onOutputResult(fakeOutput2.outputNumber, OutputResult.from(fakeOutput2))
         outputDistributor.close()
         outputDistributor.startWith(pendingOutput1) // Note: Would normally match fakeOutput1
         outputDistributor.startWith(pendingOutput2) // Note: Would normally match fakeOutput2
@@ -258,8 +277,8 @@
         outputDistributor.startWith(pendingOutput1) // Note: Would normally match fakeOutput1
         outputDistributor.startWith(pendingOutput2) // Note: Would normally match fakeOutput2
         outputDistributor.close()
-        outputDistributor.onOutputAvailable(fakeOutput1.outputNumber, fakeOutput1)
-        outputDistributor.onOutputAvailable(fakeOutput2.outputNumber, fakeOutput2)
+        outputDistributor.onOutputResult(fakeOutput1.outputNumber, OutputResult.from(fakeOutput1))
+        outputDistributor.onOutputResult(fakeOutput2.outputNumber, OutputResult.from(fakeOutput2))
 
         // If we have valid start events, but then receive close, and then receive matching outputs,
         // ensure all outputs are still considered dropped.
@@ -285,7 +304,7 @@
         outputDistributor.startWith(pendingOutput1) // Note! Out of order start event
 
         // Complete Output 1
-        outputDistributor.onOutputAvailable(fakeOutput1.outputNumber, fakeOutput1)
+        outputDistributor.onOutputResult(fakeOutput1.outputNumber, OutputResult.from(fakeOutput1))
 
         assertThat(pendingOutput1.isComplete).isTrue()
         assertThat(pendingOutput2.isComplete).isFalse() // Since 1 was out of order, do not cancel
@@ -307,7 +326,7 @@
         outputDistributor.startWith(pendingOutput3) // Out of order (relative to 4)
 
         // Complete output 3!
-        outputDistributor.onOutputAvailable(fakeOutput3.outputNumber, fakeOutput3)
+        outputDistributor.onOutputResult(fakeOutput3.outputNumber, OutputResult.from(fakeOutput3))
 
         assertThat(pendingOutput1.isComplete).isTrue() // Cancelled. 1 < 3
         assertThat(pendingOutput2.isComplete).isTrue() // Cancelled. 2 < 3
@@ -331,12 +350,12 @@
         outputDistributor.startWith(pendingOutput4) // Normal (> 3)
 
         // Normal outputs complete
-        outputDistributor.onOutputAvailable(fakeOutput2.outputNumber, fakeOutput2)
-        outputDistributor.onOutputAvailable(fakeOutput3.outputNumber, fakeOutput3)
-        outputDistributor.onOutputAvailable(fakeOutput4.outputNumber, fakeOutput4)
+        outputDistributor.onOutputResult(fakeOutput2.outputNumber, OutputResult.from(fakeOutput2))
+        outputDistributor.onOutputResult(fakeOutput3.outputNumber, OutputResult.from(fakeOutput3))
+        outputDistributor.onOutputResult(fakeOutput4.outputNumber, OutputResult.from(fakeOutput4))
 
         // Then the out of order event completes
-        outputDistributor.onOutputAvailable(fakeOutput1.outputNumber, fakeOutput1)
+        outputDistributor.onOutputResult(fakeOutput1.outputNumber, OutputResult.from(fakeOutput1))
 
         // All of the outputs are correctly distributed:
         assertThat(pendingOutput1.isComplete).isTrue()
@@ -408,12 +427,12 @@
         assertThat(pendingOutput3.isComplete).isFalse()
 
         assertThat(pendingOutput2.output).isNull()
-        assertThat(pendingOutput2.outputStatus).isEqualTo(OutputStatus.ERROR_OUTPUT_DROPPED)
+        assertThat(pendingOutput2.outputStatus).isEqualTo(OutputStatus.ERROR_OUTPUT_FAILED)
     }
 
     @Test
     fun previouslyAddedOutputIsClosedAfterFailure() {
-        outputDistributor.onOutputAvailable(fakeOutput1.outputNumber, fakeOutput1)
+        outputDistributor.onOutputResult(fakeOutput1.outputNumber, OutputResult.from(fakeOutput1))
         outputDistributor.onOutputFailure(pendingOutput1.cameraFrameNumber)
 
         // Output cannot be matched with frameNumber
@@ -425,7 +444,7 @@
         assertThat(fakeOutput1.finalized).isTrue()
         assertThat(pendingOutput1.isComplete).isTrue()
         assertThat(pendingOutput1.output).isNull()
-        assertThat(pendingOutput1.outputStatus).isEqualTo(OutputStatus.ERROR_OUTPUT_DROPPED)
+        assertThat(pendingOutput1.outputStatus).isEqualTo(OutputStatus.ERROR_OUTPUT_FAILED)
     }
 
     /**
@@ -450,8 +469,7 @@
             cameraTimestamp: CameraTimestamp,
             outputSequence: Long,
             outputNumber: Long,
-            outputStatus: OutputStatus,
-            output: FakeOutput?
+            outputResult: OutputResult<FakeOutput>
         ) {
             // Assert that this callback has only been invoked once.
             assertThat(_complete.compareAndSet(expect = false, update = true)).isTrue()
@@ -463,8 +481,8 @@
 
             // Record the actual output and outputSequence for future checks.
             this.outputSequence = outputSequence
-            this.outputStatus = outputStatus
-            this.output = output
+            this.outputStatus = outputResult.status
+            this.output = outputResult.output
         }
     }
 
diff --git a/camera/camera-camera2-pipe/src/test/java/androidx/camera/camera2/pipe/internal/OutputResultTest.kt b/camera/camera-camera2-pipe/src/test/java/androidx/camera/camera2/pipe/internal/OutputResultTest.kt
new file mode 100644
index 0000000..df25eb1
--- /dev/null
+++ b/camera/camera-camera2-pipe/src/test/java/androidx/camera/camera2/pipe/internal/OutputResultTest.kt
@@ -0,0 +1,113 @@
+/*
+ * Copyright 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.camera.camera2.pipe.internal
+
+import android.os.Build
+import androidx.camera.camera2.pipe.OutputStatus
+import androidx.camera.camera2.pipe.internal.OutputResult.Companion.completeWithFailure
+import androidx.camera.camera2.pipe.internal.OutputResult.Companion.completeWithOutput
+import androidx.camera.camera2.pipe.internal.OutputResult.Companion.outputOrNull
+import androidx.camera.camera2.pipe.internal.OutputResult.Companion.outputStatus
+import com.google.common.truth.Truth.assertThat
+import kotlinx.coroutines.CompletableDeferred
+import org.junit.Test
+import org.junit.runner.RunWith
+import org.robolectric.RobolectricTestRunner
+import org.robolectric.annotation.Config
+
+@RunWith(RobolectricTestRunner::class)
+@Config(minSdk = Build.VERSION_CODES.LOLLIPOP)
+class OutputResultTest {
+
+    @Test
+    fun outputResultCanBeCreatedWithObjects() {
+        val value = Any()
+        val result = OutputResult.from(value)
+
+        assertThat(result.available).isTrue()
+        assertThat(result.output).isSameInstanceAs(value)
+        assertThat(result.status).isEqualTo(OutputStatus.AVAILABLE)
+    }
+
+    @Test
+    fun outputResultsCanFail() {
+        val result = OutputResult.failure<Any>(OutputStatus.ERROR_OUTPUT_ABORTED)
+
+        assertThat(result.available).isFalse()
+        assertThat(result.output).isNull()
+        assertThat(result.status).isEqualTo(OutputStatus.ERROR_OUTPUT_ABORTED)
+    }
+
+    @Test
+    fun outputResultWorkWithIntegers() {
+        // Check to make sure this works with integers since OutputStatus is an inline value class.
+        val value = 42
+        val result = OutputResult.from(value)
+
+        assertThat(result.available).isTrue()
+        assertThat(result.output).isSameInstanceAs(value)
+        assertThat(result.status).isEqualTo(OutputStatus.AVAILABLE)
+
+        val failed = OutputResult.failure<Int>(OutputStatus.ERROR_OUTPUT_DROPPED)
+
+        assertThat(failed.available).isFalse()
+        assertThat(failed.output).isNull()
+        assertThat(failed.status).isEqualTo(OutputStatus.ERROR_OUTPUT_DROPPED)
+    }
+
+    @Test
+    fun outputResultWithNullableTypesAccuratelyHandleFailure() {
+        val result = OutputResult.from(null)
+
+        assertThat(result.available).isFalse()
+        assertThat(result.failure).isFalse()
+        assertThat(result.status).isEqualTo(OutputStatus.UNAVAILABLE)
+        assertThat<Any?>(result.output).isNull()
+    }
+
+    @Test
+    fun deferredWithOutputResultCompleteWithRealValue() {
+        val value = 42
+        val deferred = CompletableDeferred<OutputResult<Int>>()
+
+        assertThat(deferred.outputStatus()).isEqualTo(OutputStatus.PENDING)
+        assertThat(deferred.outputOrNull()).isNull()
+
+        deferred.completeWithOutput(value)
+
+        assertThat(deferred.outputStatus()).isEqualTo(OutputStatus.AVAILABLE)
+        assertThat(deferred.outputOrNull()).isEqualTo(value)
+    }
+
+    @Test
+    fun deferredWithOutputResultCanBeCanceled() {
+        val deferred = CompletableDeferred<OutputResult<Int>>()
+        deferred.cancel()
+
+        assertThat(deferred.outputStatus()).isEqualTo(OutputStatus.UNAVAILABLE)
+        assertThat(deferred.outputOrNull()).isNull()
+    }
+
+    @Test
+    fun deferredWithOutputResultCanBeFailedWithStatus() {
+        val deferred = CompletableDeferred<OutputResult<Int>>()
+        deferred.completeWithFailure(OutputStatus.ERROR_OUTPUT_DROPPED)
+
+        assertThat(deferred.outputStatus()).isEqualTo(OutputStatus.ERROR_OUTPUT_DROPPED)
+        assertThat(deferred.outputOrNull()).isNull()
+    }
+}
diff --git a/camera/camera-camera2-pipe/src/test/java/androidx/camera/camera2/pipe/testing/FakeImageSource.kt b/camera/camera-camera2-pipe/src/test/java/androidx/camera/camera2/pipe/testing/FakeImageSource.kt
new file mode 100644
index 0000000..e72bdc1
--- /dev/null
+++ b/camera/camera-camera2-pipe/src/test/java/androidx/camera/camera2/pipe/testing/FakeImageSource.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.camera.camera2.pipe.testing
+
+import android.os.Build
+import android.util.Size
+import android.view.Surface
+import androidx.annotation.RequiresApi
+import androidx.camera.camera2.pipe.OutputId
+import androidx.camera.camera2.pipe.StreamFormat
+import androidx.camera.camera2.pipe.StreamId
+import androidx.camera.camera2.pipe.media.ImageSource
+import androidx.camera.camera2.pipe.media.ImageSourceListener
+import kotlin.reflect.KClass
+
+@RequiresApi(Build.VERSION_CODES.LOLLIPOP)
+class FakeImageSource(
+    private val streamId: StreamId,
+    private val streamFormat: StreamFormat,
+    private val outputSizes: Map<OutputId, Size>,
+) : ImageSource {
+    private var listener: ImageSourceListener? = null
+    private val fakeSurface = FakeSurfaces.create(outputSizes.values.first())
+    override val surface: Surface
+        get() = fakeSurface
+
+    fun simulateImage(
+        timestamp: Long,
+        outputId: OutputId? = null,
+    ): FakeImage {
+        val id = outputId ?: outputSizes.keys.single()
+        val size = outputSizes[id]!!
+        val fakeImage = FakeImage(size.width, size.height, streamFormat.value, timestamp)
+        listener?.onImage(streamId, id, timestamp, fakeImage)
+        return fakeImage
+    }
+
+    fun simulateMissingImage(timestamp: Long, outputId: OutputId? = null) {
+        val id = outputId ?: outputSizes.keys.single()
+        listener?.onImage(streamId, id, timestamp, null)
+    }
+
+    override fun setListener(listener: ImageSourceListener) {
+        this.listener = listener
+    }
+
+    override fun <T : Any> unwrapAs(type: KClass<T>): T? = null
+
+    override fun close() {
+        listener = null
+        fakeSurface.release()
+    }
+}
diff --git a/camera/camera-camera2-pipe/src/test/java/androidx/camera/camera2/pipe/testing/ImageSimulator.kt b/camera/camera-camera2-pipe/src/test/java/androidx/camera/camera2/pipe/testing/ImageSimulator.kt
new file mode 100644
index 0000000..332daf1
--- /dev/null
+++ b/camera/camera-camera2-pipe/src/test/java/androidx/camera/camera2/pipe/testing/ImageSimulator.kt
@@ -0,0 +1,79 @@
+/*
+ * Copyright 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR 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.testing
+
+import android.os.Build
+import androidx.annotation.RequiresApi
+import androidx.camera.camera2.pipe.CameraGraph
+import androidx.camera.camera2.pipe.CameraMetadata
+import androidx.camera.camera2.pipe.CameraStream
+import androidx.camera.camera2.pipe.OutputId
+import androidx.camera.camera2.pipe.StreamGraph
+import androidx.camera.camera2.pipe.StreamId
+import androidx.camera.camera2.pipe.graph.StreamGraphImpl
+import androidx.camera.camera2.pipe.media.ImageSource
+
+@RequiresApi(Build.VERSION_CODES.LOLLIPOP)
+class ImageSimulator(
+    streamConfigs: List<CameraStream.Config>,
+    imageStreams: Set<CameraStream.Config>? = null,
+    defaultCameraMetadata: CameraMetadata? = null,
+    defaultStreamGraph: StreamGraph? = null
+) : AutoCloseable {
+    private val fakeSurfaces = FakeSurfaces()
+
+    val cameraMetadata = defaultCameraMetadata ?: FakeCameraMetadata()
+    val graphConfig = CameraGraph.Config(camera = cameraMetadata.camera, streams = streamConfigs)
+    val streamGraph = defaultStreamGraph ?: StreamGraphImpl(cameraMetadata, graphConfig)
+
+    private val fakeImageSources = buildMap {
+        for (config in graphConfig.streams) {
+            if (imageStreams != null && !imageStreams.contains(config)) continue
+            val cameraStream = streamGraph[config]!!
+            val fakeImageSource = FakeImageSource(
+                cameraStream.id,
+                config.outputs.first().format,
+                cameraStream.outputs.associate { it.id to it.size }
+            )
+            check(this[cameraStream.id] == null)
+            this[cameraStream.id] = fakeImageSource
+        }
+    }
+
+    val imageSources: Map<StreamId, ImageSource> = fakeImageSources
+    val imageStreams = imageSources.keys
+    val streamToSurfaceMap = buildMap {
+        for (config in graphConfig.streams) {
+            val cameraStream = streamGraph[config]!!
+            this[cameraStream.id] =
+                imageSources[cameraStream.id]?.surface ?: fakeSurfaces.createFakeSurface(
+                    cameraStream.outputs.first().size
+                )
+        }
+    }
+
+    fun simulateImage(streamId: StreamId, timestamp: Long, outputId: OutputId? = null): FakeImage {
+        return fakeImageSources[streamId]!!.simulateImage(timestamp, outputId)
+    }
+
+    override fun close() {
+        for (imageSource in fakeImageSources.values) {
+            imageSource.close()
+        }
+        fakeSurfaces.close()
+    }
+}
diff --git a/camera/camera-camera2/src/androidTest/java/androidx/camera/camera2/internal/Camera2CameraImplTest.java b/camera/camera-camera2/src/androidTest/java/androidx/camera/camera2/internal/Camera2CameraImplTest.java
index b38cbf3..0a7ca49 100644
--- a/camera/camera-camera2/src/androidTest/java/androidx/camera/camera2/internal/Camera2CameraImplTest.java
+++ b/camera/camera-camera2/src/androidTest/java/androidx/camera/camera2/internal/Camera2CameraImplTest.java
@@ -1155,7 +1155,7 @@
                 ((Camera2CameraCaptureResult) captor.getValue()).getCaptureResult();
 
         assertThat(captureResult.get(CaptureResult.CONTROL_CAPTURE_INTENT))
-                .isEqualTo(CaptureRequest.CONTROL_CAPTURE_INTENT_ZERO_SHUTTER_LAG);
+                .isEqualTo(CaptureRequest.CONTROL_CAPTURE_INTENT_PREVIEW);
         assertThat(
                 mCamera2CameraImpl.getCameraControlInternal().isZslDisabledByByUserCaseConfig())
                 .isFalse();
diff --git a/camera/camera-core/src/main/cpp/CMakeLists.txt b/camera/camera-core/src/main/cpp/CMakeLists.txt
index eeddf30..8293c0f 100644
--- a/camera/camera-core/src/main/cpp/CMakeLists.txt
+++ b/camera/camera-core/src/main/cpp/CMakeLists.txt
@@ -28,4 +28,8 @@
 find_package(libyuv REQUIRED)
 
 target_link_libraries(image_processing_util_jni PRIVATE ${log-lib} ${android-lib} ${jnigraphics-lib} libyuv::yuv)
-
+target_link_options(
+        image_processing_util_jni
+        PRIVATE
+        "-Wl,-z,max-page-size=16384"
+)
diff --git a/camera/camera-core/src/main/java/androidx/camera/core/impl/utils/TransformUtils.java b/camera/camera-core/src/main/java/androidx/camera/core/impl/utils/TransformUtils.java
index 4d78895..9c7be2d 100644
--- a/camera/camera-core/src/main/java/androidx/camera/core/impl/utils/TransformUtils.java
+++ b/camera/camera-core/src/main/java/androidx/camera/core/impl/utils/TransformUtils.java
@@ -157,6 +157,48 @@
     }
 
     /**
+     * Checks if the matrix contains a mirroring.
+     *
+     * <p>This is mostly for testing if a sensor-to-buffer transformation. This method returns true
+     * if the image has been mirrored by the pipeline.
+     */
+    public static boolean isMirrored(@NonNull Matrix matrix) {
+        // We create 2 vectors, (0, 1) and (1, 0) with -90 degrees angle between them. Then we map
+        // the vectors with the matrix. If the angle changes to positive(90 degrees), we know that
+        // the matrix contains a mirroring.
+        float[] vectors = new float[]{0, 1, 1, 0};
+        matrix.mapVectors(vectors);
+        return calculateSignedAngle(vectors[0], vectors[1], vectors[2], vectors[3]) > 0;
+    }
+
+    /**
+     * Calculates the clockwise angle between 2 vectors.
+     */
+    public static float calculateSignedAngle(float v1x, float v1y, float v2x, float v2y) {
+        // Calculate the dot product
+        float dotProduct = v1x * v2x + v1y * v2y;
+
+        // Calculate the determinant (which is proportional to the sine of the angle)
+        float det = v1x * v2y - v1y * v2x;
+
+        // Calculate the magnitudes of the vectors
+        double magV1 = Math.sqrt(v1x * v1x + v1y * v1y);
+        double magV2 = Math.sqrt(v2x * v2x + v2y * v2y);
+
+        // Calculate the cosine and sine of the angle
+        double cosTheta = dotProduct / (magV1 * magV2);
+        double sinTheta = det / (magV1 * magV2);
+
+        // Calculate the angle in radians using atan2 (result ranges from -π to π)
+        double angleRad = Math.atan2(sinTheta, cosTheta);
+
+        // Convert the angle to degrees, if needed
+        double angleDeg = Math.toDegrees(angleRad);
+
+        return (float) angleDeg;
+    }
+
+    /**
      * Gets the size after cropping and rotating.
      *
      * @return rotated size
diff --git a/camera/camera-core/src/main/java/androidx/camera/core/processing/SurfaceProcessorNode.java b/camera/camera-core/src/main/java/androidx/camera/core/processing/SurfaceProcessorNode.java
index 12c14d0..dd3088d 100644
--- a/camera/camera-core/src/main/java/androidx/camera/core/processing/SurfaceProcessorNode.java
+++ b/camera/camera-core/src/main/java/androidx/camera/core/processing/SurfaceProcessorNode.java
@@ -137,16 +137,30 @@
         // Calculate sensorToBufferTransform
         android.graphics.Matrix sensorToBufferTransform =
                 new android.graphics.Matrix(input.getSensorToBufferTransform());
-        android.graphics.Matrix imageTransform = getRectToRect(
+        android.graphics.Matrix newTransform = getRectToRect(
                 new RectF(cropRect),
                 sizeToRectF(outConfig.getSize()), rotationDegrees, mirroring);
-        sensorToBufferTransform.postConcat(imageTransform);
+        sensorToBufferTransform.postConcat(newTransform);
 
         // The aspect ratio of the output must match the aspect ratio of the crop rect. Otherwise
         // the output will be stretched.
         Size rotatedCropSize = getRotatedSize(cropRect, rotationDegrees);
         checkArgument(isAspectRatioMatchingWithRoundingError(rotatedCropSize, outConfig.getSize()));
 
+        // Calculate the transformed crop rect.
+        Rect newCropRect;
+        if (outConfig.shouldRespectInputCropRect()) {
+            checkArgument(outConfig.getCropRect().contains(input.getCropRect()),
+                    String.format("Output crop rect %s must contain input crop rect %s",
+                            outConfig.getCropRect(), input.getCropRect()));
+            newCropRect = new Rect();
+            RectF newCropRectF = new RectF(input.getCropRect());
+            newTransform.mapRect(newCropRectF);
+            newCropRectF.round(newCropRect);
+        } else {
+            newCropRect = sizeToRect(outConfig.getSize());
+        }
+
         // Copy the stream spec from the input to the output, except for the resolution.
         StreamSpec streamSpec = input.getStreamSpec().toBuilder().setResolution(
                 outConfig.getSize()).build();
@@ -158,8 +172,7 @@
                 sensorToBufferTransform,
                 // The Surface transform cannot be carried over during buffer copy.
                 /*hasCameraTransform=*/false,
-                // Crop rect is always the full size.
-                sizeToRect(outConfig.getSize()),
+                newCropRect,
                 /*rotationDegrees=*/input.getRotationDegrees() - rotationDegrees,
                 // Once copied, the target rotation is no longer useful.
                 /*targetRotation*/ ROTATION_NOT_SPECIFIED,
@@ -402,11 +415,28 @@
         public abstract int getRotationDegrees();
 
         /**
-         * The whether the stream should be mirrored.
+         * Whether the stream should be mirrored.
          */
         public abstract boolean isMirroring();
 
         /**
+         * Whether the node should respect the input's crop rect.
+         *
+         * <p>If true, the output's crop rect will be calculated based
+         * {@link OutConfig#getCropRect()} AND the input's crop rect. In this case, the
+         * {@link OutConfig#getCropRect()} must contain the input's crop rect. This applies to
+         * the scenario where the input crop rect is valid but the current node cannot apply crop
+         * rect. For example, when
+         * {@link CameraEffect#TRANSFORMATION_CAMERA_AND_SURFACE_ROTATION} option is used.
+         *
+         * <p>If false, then the node will override input's crop rect with
+         * {@link OutConfig#getCropRect()}. This mostly applies to the sharing node. For example,
+         * the children want to crop the input stream to different sizes, in which case, the
+         * input crop rect is invalid.
+         */
+        public abstract boolean shouldRespectInputCropRect();
+
+        /**
          * Creates an {@link OutConfig} instance from the input edge.
          *
          * <p>The result is an output edge with the input's transformation applied.
@@ -423,6 +453,8 @@
 
         /**
          * Creates an {@link OutConfig} instance with custom transformations.
+         *
+         * // TODO: remove this method and make the shouldRespectInputCropRect bit explicit.
          */
         @NonNull
         public static OutConfig of(@CameraEffect.Targets int targets,
@@ -431,8 +463,23 @@
                 @NonNull Size size,
                 int rotationDegrees,
                 boolean mirroring) {
+            return of(targets, format, cropRect, size, rotationDegrees, mirroring,
+                    /*shouldRespectInputCropRect=*/false);
+        }
+
+        /**
+         * Creates an {@link OutConfig} instance with custom transformations.
+         */
+        @NonNull
+        public static OutConfig of(@CameraEffect.Targets int targets,
+                @CameraEffect.Formats int format,
+                @NonNull Rect cropRect,
+                @NonNull Size size,
+                int rotationDegrees,
+                boolean mirroring,
+                boolean shouldRespectInputCropRect) {
             return new AutoValue_SurfaceProcessorNode_OutConfig(randomUUID(), targets, format,
-                    cropRect, size, rotationDegrees, mirroring);
+                    cropRect, size, rotationDegrees, mirroring, shouldRespectInputCropRect);
         }
     }
 }
diff --git a/camera/camera-core/src/main/java/androidx/camera/core/streamsharing/StreamSharing.java b/camera/camera-core/src/main/java/androidx/camera/core/streamsharing/StreamSharing.java
index 149cefe..687a41b 100644
--- a/camera/camera-core/src/main/java/androidx/camera/core/streamsharing/StreamSharing.java
+++ b/camera/camera-core/src/main/java/androidx/camera/core/streamsharing/StreamSharing.java
@@ -25,6 +25,7 @@
 import static androidx.camera.core.impl.UseCaseConfig.OPTION_CAPTURE_TYPE;
 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.sizeToRect;
 import static androidx.core.util.Preconditions.checkNotNull;
 
 import static java.util.Collections.singletonList;
@@ -321,15 +322,16 @@
         // Transform the camera edge to get the input edge.
         mEffectNode = new SurfaceProcessorNode(camera,
                 getEffect().createSurfaceProcessorInternal());
-        // Effect does not apply rotation.
         int rotationAppliedByEffect = getRotationAppliedByEffect();
+        Rect cropRectAppliedByEffect = getCropRectAppliedByEffect(cameraEdge);
         SurfaceProcessorNode.OutConfig outConfig = SurfaceProcessorNode.OutConfig.of(
                 cameraEdge.getTargets(),
                 cameraEdge.getFormat(),
-                cameraEdge.getCropRect(),
-                getRotatedSize(cameraEdge.getCropRect(), rotationAppliedByEffect),
+                cropRectAppliedByEffect,
+                getRotatedSize(cropRectAppliedByEffect, rotationAppliedByEffect),
                 rotationAppliedByEffect,
-                /*mirroring=*/false); // Effects does not mirror.
+                getMirroringAppliedByEffect(),
+                /*shouldRespectInputCropRect=*/true);
         SurfaceProcessorNode.In in = SurfaceProcessorNode.In.of(cameraEdge,
                 singletonList(outConfig));
         SurfaceProcessorNode.Out out = mEffectNode.transform(in);
@@ -348,6 +350,34 @@
         }
     }
 
+    private boolean getMirroringAppliedByEffect() {
+        CameraEffect effect = checkNotNull(getEffect());
+        if (effect.getTransformation() == CameraEffect.TRANSFORMATION_CAMERA_AND_SURFACE_ROTATION) {
+            // TODO: handle this option in VideoCapture.
+            // For a Surface that connects to the front camera directly, the texture
+            // transformation contains mirroring bit which will be applied by libraries using the
+            // TRANSFORMATION_CAMERA_AND_SURFACE_ROTATION option.
+            CameraInternal camera = checkNotNull(getCamera());
+            return camera.isFrontFacing() && camera.getHasTransform();
+        } else {
+            // By default, the effect node does not apply any mirroring.
+            return false;
+        }
+    }
+
+    private Rect getCropRectAppliedByEffect(SurfaceEdge cameraEdge) {
+        CameraEffect effect = checkNotNull(getEffect());
+        if (effect.getTransformation() == CameraEffect.TRANSFORMATION_CAMERA_AND_SURFACE_ROTATION) {
+            // TODO: handle this option in VideoCapture.
+            // Do not apply the crop rect if the effect is configured to do so.
+            Size parentSize = cameraEdge.getStreamSpec().getResolution();
+            return sizeToRect(parentSize);
+        } else {
+            // By default, the effect node does not apply any crop rect.
+            return cameraEdge.getCropRect();
+        }
+    }
+
     private void addCameraErrorListener(
             @NonNull SessionConfig.Builder sessionConfigBuilder,
             @NonNull String cameraId,
diff --git a/camera/camera-core/src/main/java/androidx/camera/core/streamsharing/VirtualCameraAdapter.java b/camera/camera-core/src/main/java/androidx/camera/core/streamsharing/VirtualCameraAdapter.java
index 66cff98b..36c58e1 100644
--- a/camera/camera-core/src/main/java/androidx/camera/core/streamsharing/VirtualCameraAdapter.java
+++ b/camera/camera-core/src/main/java/androidx/camera/core/streamsharing/VirtualCameraAdapter.java
@@ -26,6 +26,7 @@
 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.getRotationDegrees;
+import static androidx.camera.core.impl.utils.TransformUtils.isMirrored;
 import static androidx.camera.core.impl.utils.TransformUtils.rotateSize;
 import static androidx.camera.core.impl.utils.TransformUtils.within360;
 import static androidx.camera.core.streamsharing.DynamicRangeUtils.resolveDynamicRange;
@@ -211,8 +212,11 @@
     Map<UseCase, OutConfig> getChildrenOutConfigs(@NonNull SurfaceEdge sharingInputEdge,
             @ImageOutputConfig.RotationValue int parentTargetRotation, boolean isViewportSet) {
         Map<UseCase, OutConfig> outConfigs = new HashMap<>();
+        // TODO: we might be able to extract parent rotation degrees from the input edge's
+        //  sensor-to-buffer matrix and the mirroring bit.
         int parentRotationDegrees = mParentCamera.getCameraInfo().getSensorRotationDegrees(
                 parentTargetRotation);
+        boolean parentIsMirrored = isMirrored(sharingInputEdge.getSensorToBufferTransform());
         for (UseCase useCase : mChildren) {
             Pair<Rect, Size> preferredSizePair = mResolutionsMerger.getPreferredChildSizePair(
                     requireNonNull(mChildrenConfigsMap.get(useCase)),
@@ -234,7 +238,8 @@
                     cropRectBeforeScaling,
                     rotateSize(childSizeToScale, childParentDelta),
                     childParentDelta,
-                    useCase.isMirroringRequired(mParentCamera)));
+                    // Only mirror if the parent and the child disagrees.
+                    useCase.isMirroringRequired(mParentCamera) ^ parentIsMirrored));
         }
         return outConfigs;
     }
diff --git a/camera/camera-core/src/test/java/androidx/camera/core/impl/utils/TransformUtilsTest.java b/camera/camera-core/src/test/java/androidx/camera/core/impl/utils/TransformUtilsTest.java
index a1f5fa1..2aad05f 100644
--- a/camera/camera-core/src/test/java/androidx/camera/core/impl/utils/TransformUtilsTest.java
+++ b/camera/camera-core/src/test/java/androidx/camera/core/impl/utils/TransformUtilsTest.java
@@ -16,6 +16,7 @@
 
 package androidx.camera.core.impl.utils;
 
+import static androidx.camera.core.impl.utils.TransformUtils.calculateSignedAngle;
 import static androidx.camera.core.impl.utils.TransformUtils.getExifTransform;
 import static androidx.camera.core.impl.utils.TransformUtils.getRotationDegrees;
 import static androidx.camera.core.impl.utils.TransformUtils.rectToVertices;
@@ -54,6 +55,48 @@
     }
 
     @Test
+    public void calculateSignedAngles() {
+        assertThat(calculateSignedAngle(0f, 1f, 1f, 0f)).isWithin(1e-3f).of(-90);
+        assertThat(calculateSignedAngle(1f, 0f, 0f, 1f)).isWithin(1e-3f).of(90);
+    }
+
+    @Test
+    public void mirrorHorizontally_isMirrored() {
+        // Arrange.
+        Matrix matrix = new Matrix();
+        // Act.
+        matrix.postScale(1, -1);
+        // Assert.
+        assertThat(TransformUtils.isMirrored(matrix)).isTrue();
+    }
+
+    @Test
+    public void mirrorVertically_isMirrored() {
+        // Arrange.
+        Matrix matrix = new Matrix();
+        // Act.
+        matrix.postScale(-1, 1);
+        // Assert.
+        assertThat(TransformUtils.isMirrored(matrix)).isTrue();
+    }
+
+    @Test
+    public void newMatrix_isNotMirrored() {
+        assertThat(TransformUtils.isMirrored(new Matrix())).isFalse();
+    }
+
+    @Test
+    public void mirrorHorizontallyAndVertically_isNotMirrored() {
+        // Arrange.
+        Matrix matrix = new Matrix();
+        // Act.
+        matrix.postScale(1, -1);
+        matrix.postScale(-1, 1);
+        // Assert.
+        assertThat(TransformUtils.isMirrored(matrix)).isFalse();
+    }
+
+    @Test
     public void rotateSize_multipleOf90() {
         Size size = new Size(WIDTH, HEIGHT);
         //noinspection SuspiciousNameCombination
diff --git a/camera/camera-core/src/test/java/androidx/camera/core/processing/SurfaceProcessorNodeTest.kt b/camera/camera-core/src/test/java/androidx/camera/core/processing/SurfaceProcessorNodeTest.kt
index 30b170b..8549b34 100644
--- a/camera/camera-core/src/test/java/androidx/camera/core/processing/SurfaceProcessorNodeTest.kt
+++ b/camera/camera-core/src/test/java/androidx/camera/core/processing/SurfaceProcessorNodeTest.kt
@@ -37,6 +37,7 @@
 import androidx.camera.core.impl.StreamSpec
 import androidx.camera.core.impl.utils.TransformUtils
 import androidx.camera.core.impl.utils.TransformUtils.getRectToRect
+import androidx.camera.core.impl.utils.TransformUtils.getRotatedSize
 import androidx.camera.core.impl.utils.TransformUtils.is90or270
 import androidx.camera.core.impl.utils.TransformUtils.rectToSize
 import androidx.camera.core.impl.utils.TransformUtils.sizeToRect
@@ -119,6 +120,69 @@
     }
 
     @Test
+    fun respectInputCropRect_outputCropRectIsBasedOnInput() {
+        // Arrange: create a input edge and a out config. The out config's crop rect contains the
+        // input edge's crop rect.
+        val inputEdge = SurfaceEdge(
+            PREVIEW,
+            INTERNAL_DEFINED_IMAGE_FORMAT_PRIVATE,
+            StreamSpec.builder(INPUT_SIZE).build(),
+            Matrix(),
+            true,
+            Rect(160, 120, 480, 360), // 320 x 240 crop rect in the center
+            0,
+            ROTATION_NOT_SPECIFIED,
+            true
+        )
+        val outCropRect = Rect(80, 60, 560, 420)
+        val outConfig = OutConfig.of(
+            inputEdge.targets,
+            inputEdge.format,
+            outCropRect,
+            rectToSize(outCropRect),
+            inputEdge.rotationDegrees,
+            inputEdge.isMirroring,
+            true
+        )
+        createSurfaceProcessorNode()
+        // Act: transform input.
+        val out = node.transform(SurfaceProcessorNode.In.of(inputEdge, listOf(outConfig)))
+        // Assert: output crop rect is based on input crop rect AND the OutConfig crop rect.
+        assertThat(out[outConfig]!!.cropRect).isEqualTo(Rect(80, 60, 400, 300))
+    }
+
+    @Test(expected = IllegalArgumentException::class)
+    fun outConfigCropRectDoesNotContainInput_throwException() {
+        // Arrange: create a input edge and a out config. The out config's crop rect does not
+        // contain the input edge's crop rect.
+        val inputEdge = SurfaceEdge(
+            PREVIEW,
+            INTERNAL_DEFINED_IMAGE_FORMAT_PRIVATE,
+            StreamSpec.builder(INPUT_SIZE).build(),
+            Matrix(),
+            true,
+            Rect(160, 120, 480, 360), // 320 x 240 crop rect in the center
+            0,
+            ROTATION_NOT_SPECIFIED,
+            true
+        )
+        // A crop rect that is smaller than the input's.
+        val smallCropRect = Rect(170, 120, 480, 360)
+        val outConfig = OutConfig.of(
+            inputEdge.targets,
+            inputEdge.format,
+            smallCropRect,
+            rectToSize(smallCropRect),
+            inputEdge.rotationDegrees,
+            inputEdge.isMirroring,
+            true
+        )
+        createSurfaceProcessorNode()
+        // Act: transform input which throws exception.
+        node.transform(SurfaceProcessorNode.In.of(inputEdge, listOf(outConfig)))
+    }
+
+    @Test
     fun transformInput_getCorrectSensorToBufferMatrix() {
         // Arrange.
         createSurfaceProcessorNode()
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 bfeac2b..e427b91 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
@@ -18,6 +18,7 @@
 
 import android.content.Context
 import android.graphics.ImageFormat
+import android.graphics.Rect
 import android.graphics.SurfaceTexture
 import android.hardware.camera2.CameraCaptureSession
 import android.hardware.camera2.CameraDevice
@@ -112,6 +113,7 @@
         FakeCamera(null, FakeCameraInfoInternal(SENSOR_ROTATION, LENS_FACING_FRONT))
     private lateinit var streamSharing: StreamSharing
     private val size = Size(800, 600)
+    private val cropRect = Rect(150, 100, 750, 500)
     private lateinit var defaultConfig: UseCaseConfig<*>
     private lateinit var effectProcessor: FakeSurfaceProcessorInternal
     private lateinit var sharingProcessor: FakeSurfaceProcessorInternal
@@ -145,26 +147,32 @@
     }
 
     @Test
-    fun effectHandleRotation_remainingRotationIs0() {
+    fun effectHandleRotationAndMirroring_remainingTransformationIsEmpty() {
         // Arrange: create an effect that handles rotation.
         effect = FakeSurfaceEffect(
             PREVIEW or VIDEO_CAPTURE,
             CameraEffect.TRANSFORMATION_CAMERA_AND_SURFACE_ROTATION,
             effectProcessor
         )
-        streamSharing = StreamSharing(camera, setOf(child1), useCaseConfigFactory)
+        streamSharing = StreamSharing(frontCamera, setOf(child1), useCaseConfigFactory)
+        streamSharing.setViewPortCropRect(cropRect)
         streamSharing.effect = effect
         // Act: Bind effect and get sharing input edge.
         streamSharing.bindToCamera(frontCamera, null, defaultConfig)
         streamSharing.onSuggestedStreamSpecUpdated(StreamSpec.builder(size).build())
         // Assert: no remaining rotation because it's handled by the effect.
         assertThat(streamSharing.sharingInputEdge!!.rotationDegrees).isEqualTo(0)
+        assertThat(streamSharing.sharingInputEdge!!.cropRect).isEqualTo(
+            Rect(100, 50, 500, 650)
+        )
+        assertThat(streamSharing.sharingInputEdge!!.isMirroring).isEqualTo(false)
     }
 
     @Test
-    fun effectDoNotHandleRotation_remainingRotationIsNot0() {
+    fun effectDoNotHandleRotationAndMirroring_remainingTransformationIsNotEmpty() {
         // Arrange: create an effect that does not handle rotation.
         streamSharing = StreamSharing(camera, setOf(child1), useCaseConfigFactory)
+        streamSharing.setViewPortCropRect(cropRect)
         streamSharing.effect = effect
         // Act: bind effect.
         streamSharing.bindToCamera(frontCamera, null, defaultConfig)
@@ -172,6 +180,8 @@
         // Assert: the remaining rotation still exists because the effect doesn't handle it. It will
         // be handled by downstream pipeline.
         assertThat(streamSharing.sharingInputEdge!!.rotationDegrees).isEqualTo(SENSOR_ROTATION)
+        assertThat(streamSharing.sharingInputEdge!!.cropRect).isEqualTo(Rect(0, 0, 600, 400))
+        assertThat(streamSharing.sharingInputEdge!!.isMirroring).isEqualTo(true)
     }
 
     @Test
diff --git a/camera/camera-core/src/test/java/androidx/camera/core/streamsharing/VirtualCameraAdapterTest.kt b/camera/camera-core/src/test/java/androidx/camera/core/streamsharing/VirtualCameraAdapterTest.kt
index e25e507..9582f46 100644
--- a/camera/camera-core/src/test/java/androidx/camera/core/streamsharing/VirtualCameraAdapterTest.kt
+++ b/camera/camera-core/src/test/java/androidx/camera/core/streamsharing/VirtualCameraAdapterTest.kt
@@ -75,7 +75,7 @@
         private val CROP_RECT = Rect(0, 0, 800, 600)
 
         // Arbitrary transform to test that the transform is propagated.
-        private val SENSOR_TO_BUFFER = Matrix().apply { setScale(1f, -1f) }
+        private val SENSOR_TO_BUFFER = Matrix().apply { setRotate(90F) }
         private var receivedSessionConfigError: SessionConfig.SessionError? = null
         private val SESSION_CONFIG_WITH_SURFACE = SessionConfig.Builder()
             .addSurface(FakeDeferrableSurface(INPUT_SIZE, ImageFormat.PRIVATE))
@@ -87,9 +87,8 @@
     private val surfaceEdgesToClose = mutableListOf<SurfaceEdge>()
     private val parentCamera = FakeCamera()
     private val child1 = FakeUseCaseConfig.Builder().setTargetRotation(Surface.ROTATION_0).build()
-    private val child2 = FakeUseCaseConfig.Builder()
-        .setMirrorMode(MIRROR_MODE_ON)
-        .build()
+    private val child2 = FakeUseCaseConfig.Builder().setMirrorMode(MIRROR_MODE_ON).build()
+
     private val childrenEdges = mapOf(
         Pair(child1 as UseCase, createSurfaceEdge()),
         Pair(child2 as UseCase, createSurfaceEdge())
@@ -249,6 +248,20 @@
     }
 
     @Test
+    fun parentHasMirroring_clientDoNotApplyMirroring() {
+        // Arrange: create an edge that has mirrored the input in the past.
+        val inputEdge = createSurfaceEdge(matrix = Matrix().apply { setScale(-1f, 1f) })
+        // Act: get the children's out configs.
+        val outConfigs = adapter.getChildrenOutConfigs(inputEdge, Surface.ROTATION_90, true)
+        // Assert: child1 needs additional mirroring because the parent mirrors the input while the
+        // child doesn't mirror.
+        assertThat(outConfigs[child1]!!.isMirroring).isTrue()
+        // Assert: child2 does not need additional mirroring because both the parent and the child
+        // mirrors the input.
+        assertThat(outConfigs[child2]!!.isMirroring).isFalse()
+    }
+
+    @Test
     fun getChildrenOutConfigs() {
         // Arrange.
         val cropRect = Rect(10, 10, 410, 310)
diff --git a/camera/camera-testing/src/main/cpp/CMakeLists.txt b/camera/camera-testing/src/main/cpp/CMakeLists.txt
index b0c17f4..af959ea 100644
--- a/camera/camera-testing/src/main/cpp/CMakeLists.txt
+++ b/camera/camera-testing/src/main/cpp/CMakeLists.txt
@@ -27,4 +27,9 @@
 
 find_library(android-lib android)
 
-target_link_libraries(testing_surface_format_jni  ${android-lib})
\ No newline at end of file
+target_link_libraries(testing_surface_format_jni  ${android-lib})
+target_link_options(
+        testing_surface_format_jni
+        PRIVATE
+        "-Wl,-z,max-page-size=16384"
+)
diff --git a/camera/camera-viewfinder-core/api/current.txt b/camera/camera-viewfinder-core/api/current.txt
index db64093..e6c2771 100644
--- a/camera/camera-viewfinder-core/api/current.txt
+++ b/camera/camera-viewfinder-core/api/current.txt
@@ -80,5 +80,9 @@
   public static final class ViewfinderSurfaceRequest.Result.Companion {
   }
 
+  public final class ViewfinderSurfaceRequestUtil {
+    method @RequiresApi(21) public static androidx.camera.viewfinder.surface.ViewfinderSurfaceRequest.Builder populateFromCharacteristics(androidx.camera.viewfinder.surface.ViewfinderSurfaceRequest.Builder, android.hardware.camera2.CameraCharacteristics cameraCharacteristics);
+  }
+
 }
 
diff --git a/camera/camera-viewfinder-core/api/restricted_current.txt b/camera/camera-viewfinder-core/api/restricted_current.txt
index db64093..e6c2771 100644
--- a/camera/camera-viewfinder-core/api/restricted_current.txt
+++ b/camera/camera-viewfinder-core/api/restricted_current.txt
@@ -80,5 +80,9 @@
   public static final class ViewfinderSurfaceRequest.Result.Companion {
   }
 
+  public final class ViewfinderSurfaceRequestUtil {
+    method @RequiresApi(21) public static androidx.camera.viewfinder.surface.ViewfinderSurfaceRequest.Builder populateFromCharacteristics(androidx.camera.viewfinder.surface.ViewfinderSurfaceRequest.Builder, android.hardware.camera2.CameraCharacteristics cameraCharacteristics);
+  }
+
 }
 
diff --git a/camera/camera-viewfinder-core/src/main/java/androidx/camera/viewfinder/surface/ViewfinderSurfaceRequestExt.kt b/camera/camera-viewfinder-core/src/main/java/androidx/camera/viewfinder/surface/ViewfinderSurfaceRequestExt.kt
new file mode 100644
index 0000000..945abdb
--- /dev/null
+++ b/camera/camera-viewfinder-core/src/main/java/androidx/camera/viewfinder/surface/ViewfinderSurfaceRequestExt.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.
+ */
+@file:JvmName("ViewfinderSurfaceRequestUtil")
+
+package androidx.camera.viewfinder.surface
+
+import android.annotation.SuppressLint
+import android.hardware.camera2.CameraCharacteristics
+import android.hardware.camera2.CameraMetadata
+import androidx.annotation.RequiresApi
+
+/**
+ * Populates [ViewfinderSurfaceRequest.Builder] from [CameraCharacteristics].
+ *
+ * The [CameraCharacteristics] will be used to populate information including lens facing,
+ * sensor orientation and [ImplementationMode].
+ * If the hardware level is legacy, the [ImplementationMode] will be set to
+ * [ImplementationMode.COMPATIBLE].
+ */
+@SuppressLint("ClassVerificationFailure")
+@RequiresApi(21)
+fun ViewfinderSurfaceRequest.Builder.populateFromCharacteristics(
+    cameraCharacteristics: CameraCharacteristics
+): ViewfinderSurfaceRequest.Builder {
+    setLensFacing(cameraCharacteristics.get(CameraCharacteristics.LENS_FACING)!!)
+    setSensorOrientation(
+        cameraCharacteristics.get(CameraCharacteristics.SENSOR_ORIENTATION)!!)
+    if (cameraCharacteristics.get(CameraCharacteristics.INFO_SUPPORTED_HARDWARE_LEVEL)
+        == CameraMetadata.INFO_SUPPORTED_HARDWARE_LEVEL_LEGACY) {
+        setImplementationMode(androidx.camera.viewfinder.surface.ImplementationMode.COMPATIBLE)
+    }
+    return this
+}
diff --git a/camera/camera-viewfinder/api/current.txt b/camera/camera-viewfinder/api/current.txt
index a7333d7..82a5cb5 100644
--- a/camera/camera-viewfinder/api/current.txt
+++ b/camera/camera-viewfinder/api/current.txt
@@ -7,15 +7,17 @@
     ctor @UiThread public CameraViewfinder(android.content.Context, android.util.AttributeSet?, int);
     ctor @UiThread public CameraViewfinder(android.content.Context, android.util.AttributeSet?, int, int);
     method @UiThread public android.graphics.Bitmap? getBitmap();
-    method @UiThread public androidx.camera.viewfinder.CameraViewfinder.ImplementationMode getImplementationMode();
+    method @Deprecated @UiThread public androidx.camera.viewfinder.CameraViewfinder.ImplementationMode getImplementationMode();
     method @UiThread public androidx.camera.viewfinder.CameraViewfinder.ScaleType getScaleType();
-    method @UiThread public com.google.common.util.concurrent.ListenableFuture<android.view.Surface!> requestSurfaceAsync(androidx.camera.viewfinder.ViewfinderSurfaceRequest);
+    method @UiThread public androidx.camera.viewfinder.surface.ImplementationMode getSurfaceImplementationMode();
+    method @UiThread public com.google.common.util.concurrent.ListenableFuture<android.view.Surface!> requestSurfaceAsync(androidx.camera.viewfinder.surface.ViewfinderSurfaceRequest);
+    method @Deprecated @UiThread public com.google.common.util.concurrent.ListenableFuture<android.view.Surface!> requestSurfaceAsync(androidx.camera.viewfinder.ViewfinderSurfaceRequest);
     method @UiThread public void setScaleType(androidx.camera.viewfinder.CameraViewfinder.ScaleType);
   }
 
-  @RequiresApi(21) public enum CameraViewfinder.ImplementationMode {
-    enum_constant public static final androidx.camera.viewfinder.CameraViewfinder.ImplementationMode COMPATIBLE;
-    enum_constant public static final androidx.camera.viewfinder.CameraViewfinder.ImplementationMode PERFORMANCE;
+  @Deprecated @RequiresApi(21) public enum CameraViewfinder.ImplementationMode {
+    enum_constant @Deprecated public static final androidx.camera.viewfinder.CameraViewfinder.ImplementationMode COMPATIBLE;
+    enum_constant @Deprecated public static final androidx.camera.viewfinder.CameraViewfinder.ImplementationMode PERFORMANCE;
   }
 
   @RequiresApi(21) public enum CameraViewfinder.ScaleType {
@@ -28,30 +30,31 @@
   }
 
   @RequiresApi(21) public final class CameraViewfinderExt {
-    method public suspend Object? requestSurface(androidx.camera.viewfinder.CameraViewfinder, androidx.camera.viewfinder.ViewfinderSurfaceRequest viewfinderSurfaceRequest, kotlin.coroutines.Continuation<? super android.view.Surface>);
+    method public suspend Object? requestSurface(androidx.camera.viewfinder.CameraViewfinder, androidx.camera.viewfinder.surface.ViewfinderSurfaceRequest viewfinderSurfaceRequest, kotlin.coroutines.Continuation<? super android.view.Surface>);
+    method @Deprecated public suspend Object? requestSurface(androidx.camera.viewfinder.CameraViewfinder, androidx.camera.viewfinder.ViewfinderSurfaceRequest viewfinderSurfaceRequest, kotlin.coroutines.Continuation<? super android.view.Surface>);
     field public static final androidx.camera.viewfinder.CameraViewfinderExt INSTANCE;
   }
 
-  @RequiresApi(21) public class ViewfinderSurfaceRequest {
-    method public androidx.camera.viewfinder.CameraViewfinder.ImplementationMode? getImplementationMode();
-    method public int getLensFacing();
-    method public android.util.Size getResolution();
-    method public int getSensorOrientation();
-    method public void markSurfaceSafeToRelease();
+  @Deprecated @RequiresApi(21) public class ViewfinderSurfaceRequest {
+    method @Deprecated public androidx.camera.viewfinder.CameraViewfinder.ImplementationMode? getImplementationMode();
+    method @Deprecated public int getLensFacing();
+    method @Deprecated public android.util.Size getResolution();
+    method @Deprecated public int getSensorOrientation();
+    method @Deprecated public void markSurfaceSafeToRelease();
   }
 
-  public static final class ViewfinderSurfaceRequest.Builder {
-    ctor public ViewfinderSurfaceRequest.Builder(android.util.Size);
-    ctor public ViewfinderSurfaceRequest.Builder(androidx.camera.viewfinder.ViewfinderSurfaceRequest);
-    ctor public ViewfinderSurfaceRequest.Builder(androidx.camera.viewfinder.ViewfinderSurfaceRequest.Builder);
-    method public androidx.camera.viewfinder.ViewfinderSurfaceRequest build();
-    method public androidx.camera.viewfinder.ViewfinderSurfaceRequest.Builder setImplementationMode(androidx.camera.viewfinder.CameraViewfinder.ImplementationMode?);
-    method public androidx.camera.viewfinder.ViewfinderSurfaceRequest.Builder setLensFacing(int);
-    method public androidx.camera.viewfinder.ViewfinderSurfaceRequest.Builder setSensorOrientation(int);
+  @Deprecated public static final class ViewfinderSurfaceRequest.Builder {
+    ctor @Deprecated public ViewfinderSurfaceRequest.Builder(android.util.Size);
+    ctor @Deprecated public ViewfinderSurfaceRequest.Builder(androidx.camera.viewfinder.ViewfinderSurfaceRequest);
+    ctor @Deprecated public ViewfinderSurfaceRequest.Builder(androidx.camera.viewfinder.ViewfinderSurfaceRequest.Builder);
+    method @Deprecated public androidx.camera.viewfinder.ViewfinderSurfaceRequest build();
+    method @Deprecated public androidx.camera.viewfinder.ViewfinderSurfaceRequest.Builder setImplementationMode(androidx.camera.viewfinder.CameraViewfinder.ImplementationMode?);
+    method @Deprecated public androidx.camera.viewfinder.ViewfinderSurfaceRequest.Builder setLensFacing(int);
+    method @Deprecated public androidx.camera.viewfinder.ViewfinderSurfaceRequest.Builder setSensorOrientation(int);
   }
 
   public final class ViewfinderSurfaceRequestUtil {
-    method @RequiresApi(21) public static androidx.camera.viewfinder.ViewfinderSurfaceRequest.Builder populateFromCharacteristics(androidx.camera.viewfinder.ViewfinderSurfaceRequest.Builder, android.hardware.camera2.CameraCharacteristics cameraCharacteristics);
+    method @Deprecated @RequiresApi(21) public static androidx.camera.viewfinder.ViewfinderSurfaceRequest.Builder populateFromCharacteristics(androidx.camera.viewfinder.ViewfinderSurfaceRequest.Builder, android.hardware.camera2.CameraCharacteristics cameraCharacteristics);
   }
 
 }
diff --git a/camera/camera-viewfinder/api/restricted_current.txt b/camera/camera-viewfinder/api/restricted_current.txt
index a7333d7..82a5cb5 100644
--- a/camera/camera-viewfinder/api/restricted_current.txt
+++ b/camera/camera-viewfinder/api/restricted_current.txt
@@ -7,15 +7,17 @@
     ctor @UiThread public CameraViewfinder(android.content.Context, android.util.AttributeSet?, int);
     ctor @UiThread public CameraViewfinder(android.content.Context, android.util.AttributeSet?, int, int);
     method @UiThread public android.graphics.Bitmap? getBitmap();
-    method @UiThread public androidx.camera.viewfinder.CameraViewfinder.ImplementationMode getImplementationMode();
+    method @Deprecated @UiThread public androidx.camera.viewfinder.CameraViewfinder.ImplementationMode getImplementationMode();
     method @UiThread public androidx.camera.viewfinder.CameraViewfinder.ScaleType getScaleType();
-    method @UiThread public com.google.common.util.concurrent.ListenableFuture<android.view.Surface!> requestSurfaceAsync(androidx.camera.viewfinder.ViewfinderSurfaceRequest);
+    method @UiThread public androidx.camera.viewfinder.surface.ImplementationMode getSurfaceImplementationMode();
+    method @UiThread public com.google.common.util.concurrent.ListenableFuture<android.view.Surface!> requestSurfaceAsync(androidx.camera.viewfinder.surface.ViewfinderSurfaceRequest);
+    method @Deprecated @UiThread public com.google.common.util.concurrent.ListenableFuture<android.view.Surface!> requestSurfaceAsync(androidx.camera.viewfinder.ViewfinderSurfaceRequest);
     method @UiThread public void setScaleType(androidx.camera.viewfinder.CameraViewfinder.ScaleType);
   }
 
-  @RequiresApi(21) public enum CameraViewfinder.ImplementationMode {
-    enum_constant public static final androidx.camera.viewfinder.CameraViewfinder.ImplementationMode COMPATIBLE;
-    enum_constant public static final androidx.camera.viewfinder.CameraViewfinder.ImplementationMode PERFORMANCE;
+  @Deprecated @RequiresApi(21) public enum CameraViewfinder.ImplementationMode {
+    enum_constant @Deprecated public static final androidx.camera.viewfinder.CameraViewfinder.ImplementationMode COMPATIBLE;
+    enum_constant @Deprecated public static final androidx.camera.viewfinder.CameraViewfinder.ImplementationMode PERFORMANCE;
   }
 
   @RequiresApi(21) public enum CameraViewfinder.ScaleType {
@@ -28,30 +30,31 @@
   }
 
   @RequiresApi(21) public final class CameraViewfinderExt {
-    method public suspend Object? requestSurface(androidx.camera.viewfinder.CameraViewfinder, androidx.camera.viewfinder.ViewfinderSurfaceRequest viewfinderSurfaceRequest, kotlin.coroutines.Continuation<? super android.view.Surface>);
+    method public suspend Object? requestSurface(androidx.camera.viewfinder.CameraViewfinder, androidx.camera.viewfinder.surface.ViewfinderSurfaceRequest viewfinderSurfaceRequest, kotlin.coroutines.Continuation<? super android.view.Surface>);
+    method @Deprecated public suspend Object? requestSurface(androidx.camera.viewfinder.CameraViewfinder, androidx.camera.viewfinder.ViewfinderSurfaceRequest viewfinderSurfaceRequest, kotlin.coroutines.Continuation<? super android.view.Surface>);
     field public static final androidx.camera.viewfinder.CameraViewfinderExt INSTANCE;
   }
 
-  @RequiresApi(21) public class ViewfinderSurfaceRequest {
-    method public androidx.camera.viewfinder.CameraViewfinder.ImplementationMode? getImplementationMode();
-    method public int getLensFacing();
-    method public android.util.Size getResolution();
-    method public int getSensorOrientation();
-    method public void markSurfaceSafeToRelease();
+  @Deprecated @RequiresApi(21) public class ViewfinderSurfaceRequest {
+    method @Deprecated public androidx.camera.viewfinder.CameraViewfinder.ImplementationMode? getImplementationMode();
+    method @Deprecated public int getLensFacing();
+    method @Deprecated public android.util.Size getResolution();
+    method @Deprecated public int getSensorOrientation();
+    method @Deprecated public void markSurfaceSafeToRelease();
   }
 
-  public static final class ViewfinderSurfaceRequest.Builder {
-    ctor public ViewfinderSurfaceRequest.Builder(android.util.Size);
-    ctor public ViewfinderSurfaceRequest.Builder(androidx.camera.viewfinder.ViewfinderSurfaceRequest);
-    ctor public ViewfinderSurfaceRequest.Builder(androidx.camera.viewfinder.ViewfinderSurfaceRequest.Builder);
-    method public androidx.camera.viewfinder.ViewfinderSurfaceRequest build();
-    method public androidx.camera.viewfinder.ViewfinderSurfaceRequest.Builder setImplementationMode(androidx.camera.viewfinder.CameraViewfinder.ImplementationMode?);
-    method public androidx.camera.viewfinder.ViewfinderSurfaceRequest.Builder setLensFacing(int);
-    method public androidx.camera.viewfinder.ViewfinderSurfaceRequest.Builder setSensorOrientation(int);
+  @Deprecated public static final class ViewfinderSurfaceRequest.Builder {
+    ctor @Deprecated public ViewfinderSurfaceRequest.Builder(android.util.Size);
+    ctor @Deprecated public ViewfinderSurfaceRequest.Builder(androidx.camera.viewfinder.ViewfinderSurfaceRequest);
+    ctor @Deprecated public ViewfinderSurfaceRequest.Builder(androidx.camera.viewfinder.ViewfinderSurfaceRequest.Builder);
+    method @Deprecated public androidx.camera.viewfinder.ViewfinderSurfaceRequest build();
+    method @Deprecated public androidx.camera.viewfinder.ViewfinderSurfaceRequest.Builder setImplementationMode(androidx.camera.viewfinder.CameraViewfinder.ImplementationMode?);
+    method @Deprecated public androidx.camera.viewfinder.ViewfinderSurfaceRequest.Builder setLensFacing(int);
+    method @Deprecated public androidx.camera.viewfinder.ViewfinderSurfaceRequest.Builder setSensorOrientation(int);
   }
 
   public final class ViewfinderSurfaceRequestUtil {
-    method @RequiresApi(21) public static androidx.camera.viewfinder.ViewfinderSurfaceRequest.Builder populateFromCharacteristics(androidx.camera.viewfinder.ViewfinderSurfaceRequest.Builder, android.hardware.camera2.CameraCharacteristics cameraCharacteristics);
+    method @Deprecated @RequiresApi(21) public static androidx.camera.viewfinder.ViewfinderSurfaceRequest.Builder populateFromCharacteristics(androidx.camera.viewfinder.ViewfinderSurfaceRequest.Builder, android.hardware.camera2.CameraCharacteristics cameraCharacteristics);
   }
 
 }
diff --git a/camera/camera-viewfinder/build.gradle b/camera/camera-viewfinder/build.gradle
index 7ab055e..738e308 100644
--- a/camera/camera-viewfinder/build.gradle
+++ b/camera/camera-viewfinder/build.gradle
@@ -32,6 +32,7 @@
 
 dependencies {
     api("androidx.annotation:annotation:1.2.0")
+    api(project(':camera:camera-viewfinder-core'))
     implementation("androidx.annotation:annotation-experimental:1.4.0")
     implementation(libs.guavaListenableFuture)
     implementation("androidx.core:core:1.7.0")
@@ -42,7 +43,6 @@
     implementation("androidx.test.espresso:espresso-idling-resource:3.1.0")
     implementation(libs.kotlinCoroutinesCore)
     implementation(libs.kotlinCoroutinesAndroid)
-    implementation project(':camera:camera-viewfinder-core')
 
     annotationProcessor(libs.autoValue)
 
diff --git a/camera/camera-viewfinder/src/androidTest/java/androidx/camera/viewfinder/CameraViewfinderBitmapTest.kt b/camera/camera-viewfinder/src/androidTest/java/androidx/camera/viewfinder/CameraViewfinderBitmapTest.kt
index 20d606e..e222d09 100644
--- a/camera/camera-viewfinder/src/androidTest/java/androidx/camera/viewfinder/CameraViewfinderBitmapTest.kt
+++ b/camera/camera-viewfinder/src/androidTest/java/androidx/camera/viewfinder/CameraViewfinderBitmapTest.kt
@@ -24,6 +24,8 @@
 import androidx.camera.impl.utils.futures.FutureCallback
 import androidx.camera.impl.utils.futures.Futures
 import androidx.camera.viewfinder.CameraViewfinder.ScaleType.FILL_CENTER
+import androidx.camera.viewfinder.surface.ViewfinderSurfaceRequest
+import androidx.camera.viewfinder.surface.populateFromCharacteristics
 import androidx.camera.viewfinder.utils.CoreAppTestUtil
 import androidx.camera.viewfinder.utils.FakeActivity
 import androidx.core.content.ContextCompat
diff --git a/camera/camera-viewfinder/src/androidTest/java/androidx/camera/viewfinder/SurfaceViewImplementationTest.kt b/camera/camera-viewfinder/src/androidTest/java/androidx/camera/viewfinder/SurfaceViewImplementationTest.kt
index 4074c5e..c9e189e 100644
--- a/camera/camera-viewfinder/src/androidTest/java/androidx/camera/viewfinder/SurfaceViewImplementationTest.kt
+++ b/camera/camera-viewfinder/src/androidTest/java/androidx/camera/viewfinder/SurfaceViewImplementationTest.kt
@@ -21,6 +21,8 @@
 import android.util.Size
 import android.view.View
 import android.widget.FrameLayout
+import androidx.camera.viewfinder.surface.ViewfinderSurfaceRequest
+import androidx.camera.viewfinder.surface.populateFromCharacteristics
 import androidx.camera.viewfinder.utils.CoreAppTestUtil
 import androidx.camera.viewfinder.utils.FakeActivity
 import androidx.test.core.app.ActivityScenario
@@ -81,7 +83,7 @@
     @After
     fun tearDown() {
         if (::mSurfaceRequest.isInitialized) {
-            mSurfaceRequest.viewfinderSurface.close()
+            mSurfaceRequest.getSurface().close()
         }
     }
 
@@ -93,8 +95,8 @@
             mImplementation.onSurfaceRequested(mSurfaceRequest)
         }
 
-        mSurfaceRequest.viewfinderSurface.surface.get(1000, TimeUnit.MILLISECONDS)
-        mSurfaceRequest.viewfinderSurface.close()
+        mSurfaceRequest.getSurface().getSurfaceAsync().get(1000, TimeUnit.MILLISECONDS)
+        mSurfaceRequest.getSurface().close()
     }
 
     @Throws(Throwable::class)
diff --git a/camera/camera-viewfinder/src/androidTest/java/androidx/camera/viewfinder/TextureViewImplementationTest.kt b/camera/camera-viewfinder/src/androidTest/java/androidx/camera/viewfinder/TextureViewImplementationTest.kt
index cf9b4aa..d80ef56 100644
--- a/camera/camera-viewfinder/src/androidTest/java/androidx/camera/viewfinder/TextureViewImplementationTest.kt
+++ b/camera/camera-viewfinder/src/androidTest/java/androidx/camera/viewfinder/TextureViewImplementationTest.kt
@@ -22,6 +22,8 @@
 import android.util.Size
 import android.view.TextureView
 import android.widget.FrameLayout
+import androidx.camera.viewfinder.surface.ViewfinderSurfaceRequest
+import androidx.camera.viewfinder.surface.populateFromCharacteristics
 import androidx.test.core.app.ApplicationProvider
 import androidx.test.ext.junit.runners.AndroidJUnit4
 import androidx.test.filters.LargeTest
@@ -76,7 +78,7 @@
         if (_surfaceRequest != null) {
             _surfaceRequest!!.willNotProvideSurface()
             // Ensure all successful requests have their returned future finish.
-            _surfaceRequest!!.viewfinderSurface.close()
+            _surfaceRequest!!.getSurface().close()
             _surfaceRequest = null
         }
     }
@@ -89,7 +91,7 @@
     fun doNotProvideSurface_ifSurfaceTextureNotAvailableYet() {
         val request = surfaceRequest
         implementation!!.onSurfaceRequested(request)
-        request.viewfinderSurface.surface[2, TimeUnit.SECONDS]
+        request.getSurface().getSurfaceAsync()[2, TimeUnit.SECONDS]
     }
 
     @Test
@@ -97,7 +99,7 @@
     fun provideSurface_ifSurfaceTextureAvailable() {
         val surfaceRequest = surfaceRequest
         implementation!!.onSurfaceRequested(surfaceRequest)
-        val surfaceListenableFuture = surfaceRequest.viewfinderSurface.surface
+        val surfaceListenableFuture = surfaceRequest.getSurface().getSurfaceAsync()
         implementation!!.mTextureView
             ?.surfaceTextureListener!!
             .onSurfaceTextureAvailable(surfaceTexture!!, ANY_WIDTH, ANY_HEIGHT)
@@ -110,7 +112,7 @@
     fun doNotDestroySurface_whenSurfaceTextureBeingDestroyed_andCameraUsingSurface() {
         val surfaceRequest = surfaceRequest
         implementation!!.onSurfaceRequested(surfaceRequest)
-        val surfaceListenableFuture = surfaceRequest.viewfinderSurface.surface
+        val surfaceListenableFuture = surfaceRequest.getSurface().getSurfaceAsync()
         val surfaceTextureListener = implementation!!.mTextureView?.surfaceTextureListener
         surfaceTextureListener!!.onSurfaceTextureAvailable(surfaceTexture!!, ANY_WIDTH, ANY_HEIGHT)
         surfaceListenableFuture.get()
@@ -128,8 +130,8 @@
     fun destroySurface_whenSurfaceTextureBeingDestroyed_andCameraNotUsingSurface() {
         val surfaceRequest = surfaceRequest
         implementation!!.onSurfaceRequested(surfaceRequest)
-        val deferrableSurface = surfaceRequest.viewfinderSurface
-        val surfaceListenableFuture = deferrableSurface.surface
+        val deferrableSurface = surfaceRequest.getSurface()
+        val surfaceListenableFuture = deferrableSurface.getSurfaceAsync()
         val surfaceTextureListener = implementation!!.mTextureView?.surfaceTextureListener
         surfaceTextureListener!!.onSurfaceTextureAvailable(surfaceTexture!!, ANY_WIDTH, ANY_HEIGHT)
         surfaceListenableFuture.get()
@@ -151,8 +153,8 @@
     fun releaseSurfaceTexture_afterSurfaceTextureDestroyed_andCameraNoLongerUsingSurface() {
         val surfaceRequest = surfaceRequest
         implementation!!.onSurfaceRequested(surfaceRequest)
-        val deferrableSurface = surfaceRequest.viewfinderSurface
-        val surfaceListenableFuture = deferrableSurface.surface
+        val deferrableSurface = surfaceRequest.getSurface()
+        val surfaceListenableFuture = deferrableSurface.getSurfaceAsync()
         val surfaceTextureListener = implementation!!.mTextureView?.surfaceTextureListener
         surfaceTextureListener!!.onSurfaceTextureAvailable(surfaceTexture!!, ANY_WIDTH, ANY_HEIGHT)
         surfaceListenableFuture.get()
@@ -174,7 +176,7 @@
         val surfaceRequest = surfaceRequest
         implementation!!.onSurfaceRequested(surfaceRequest)
         // Cancel the request from the camera side
-        surfaceRequest.viewfinderSurface.surface.cancel(true)
+        surfaceRequest.getSurface().getSurfaceAsync().cancel(true)
 
         // Wait enough time for mCompleter's cancellation listener to be called
         Thread.sleep(1000)
@@ -211,8 +213,8 @@
     fun resetSurfaceTextureOnDetachAndAttachWindow() {
         val surfaceRequest = surfaceRequest
         implementation!!.onSurfaceRequested(surfaceRequest)
-        val deferrableSurface = surfaceRequest.viewfinderSurface
-        val surfaceListenableFuture = deferrableSurface.surface
+        val deferrableSurface = surfaceRequest.getSurface()
+        val surfaceListenableFuture = deferrableSurface.getSurfaceAsync()
         val surfaceTextureListener = implementation!!.mTextureView?.surfaceTextureListener
         surfaceTextureListener!!.onSurfaceTextureAvailable(surfaceTexture!!, ANY_WIDTH, ANY_HEIGHT)
         surfaceListenableFuture.get()
@@ -230,8 +232,8 @@
     fun releaseDetachedSurfaceTexture_whenDeferrableSurfaceClose() {
         val surfaceRequest = surfaceRequest
         implementation!!.onSurfaceRequested(surfaceRequest)
-        val deferrableSurface = surfaceRequest.viewfinderSurface
-        val surfaceListenableFuture = deferrableSurface.surface
+        val deferrableSurface = surfaceRequest.getSurface()
+        val surfaceListenableFuture = deferrableSurface.getSurfaceAsync()
         val surfaceTextureListener = implementation!!.mTextureView?.surfaceTextureListener
         surfaceTextureListener!!.onSurfaceTextureAvailable(surfaceTexture!!, ANY_WIDTH, ANY_HEIGHT)
         surfaceListenableFuture.get()
diff --git a/camera/camera-viewfinder/src/main/java/androidx/camera/viewfinder/CameraViewfinder.java b/camera/camera-viewfinder/src/main/java/androidx/camera/viewfinder/CameraViewfinder.java
index 9a438c0..dc194bd 100644
--- a/camera/camera-viewfinder/src/main/java/androidx/camera/viewfinder/CameraViewfinder.java
+++ b/camera/camera-viewfinder/src/main/java/androidx/camera/viewfinder/CameraViewfinder.java
@@ -45,9 +45,9 @@
 import androidx.camera.viewfinder.internal.quirk.DeviceQuirks;
 import androidx.camera.viewfinder.internal.quirk.SurfaceViewNotCroppedByParentQuirk;
 import androidx.camera.viewfinder.internal.quirk.SurfaceViewStretchedQuirk;
-import androidx.camera.viewfinder.internal.surface.ViewfinderSurfaceProvider;
 import androidx.camera.viewfinder.internal.utils.Logger;
 import androidx.camera.viewfinder.internal.utils.Threads;
+import androidx.camera.viewfinder.surface.ViewfinderSurfaceProvider;
 import androidx.core.content.ContextCompat;
 import androidx.core.view.ViewCompat;
 
@@ -66,7 +66,8 @@
     private static final String TAG = "CameraViewFinder";
 
     @ColorRes private static final int DEFAULT_BACKGROUND_COLOR = android.R.color.black;
-    private static final ImplementationMode DEFAULT_IMPL_MODE = ImplementationMode.PERFORMANCE;
+    private static final androidx.camera.viewfinder.surface.ImplementationMode DEFAULT_IMPL_MODE =
+            androidx.camera.viewfinder.surface.ImplementationMode.PERFORMANCE;
 
     // Synthetic access
     @SuppressWarnings("WeakerAccess")
@@ -80,7 +81,7 @@
     @NonNull
     private final Looper mRequiredLooper = Looper.myLooper();
 
-    @NonNull ImplementationMode mImplementationMode;
+    @NonNull androidx.camera.viewfinder.surface.ImplementationMode mImplementationMode;
 
     // Synthetic access
     @SuppressWarnings("WeakerAccess")
@@ -90,7 +91,7 @@
     // Synthetic access
     @SuppressWarnings("WeakerAccess")
     @Nullable
-    ViewfinderSurfaceRequest mCurrentSurfaceRequest;
+    androidx.camera.viewfinder.surface.ViewfinderSurfaceRequest mCurrentSurfaceRequest;
 
     private final OnLayoutChangeListener mOnLayoutChangeListener =
             (v, left, top, right, bottom, oldLeft, oldTop, oldRight, oldBottom) -> {
@@ -107,7 +108,9 @@
 
         @Override
         @AnyThread
-        public void onSurfaceRequested(@NonNull ViewfinderSurfaceRequest surfaceRequest) {
+        public void onSurfaceRequested(
+                @NonNull androidx.camera.viewfinder.surface.ViewfinderSurfaceRequest surfaceRequest
+        ) {
             if (!Threads.isMainThread()) {
                 // In short term, throwing exception to guarantee onSurfaceRequest is
                 //  called on main thread. In long term, user should be able to specify an
@@ -182,7 +185,8 @@
             int implementationModeId =
                     attributes.getInteger(R.styleable.Viewfinder_implementationMode,
                             DEFAULT_IMPL_MODE.getId());
-            mImplementationMode = ImplementationMode.fromId(implementationModeId);
+            mImplementationMode = androidx.camera.viewfinder.surface.ImplementationMode.fromId(
+                    implementationModeId);
         } finally {
             attributes.recycle();
         }
@@ -206,11 +210,40 @@
      * {@link ImplementationMode}.
      *
      * @return The {@link ImplementationMode} for {@link CameraViewfinder}.
+     * @deprecated Use {@link #getSurfaceImplementationMode()} instead.
+     * The {@link ImplementationMode} in camera-viewfinder will be made obsolete with the
+     * introduction of camera-viewfinder-core.
      */
+    @Deprecated
     @UiThread
     @NonNull
     public ImplementationMode getImplementationMode() {
         checkUiThread();
+        return ImplementationMode.fromId(mImplementationMode.getId());
+    }
+
+    /**
+     * Returns the {@link androidx.camera.viewfinder.surface.ImplementationMode}.
+     *
+     * <p> For each {@link androidx.camera.viewfinder.surface.ViewfinderSurfaceRequest} sent to
+     * {@link CameraViewfinder}, the
+     * {@link androidx.camera.viewfinder.surface.ImplementationMode} set in the
+     * {@link androidx.camera.viewfinder.surface.ViewfinderSurfaceRequest} will be used first.
+     * If it's not set, the {@code app:implementationMode} in the layout xml will be used. If
+     * it's not set in the layout xml, the default value
+     * {@link androidx.camera.viewfinder.surface.ImplementationMode#PERFORMANCE}
+     * will be used. Each {@link androidx.camera.viewfinder.surface.ViewfinderSurfaceRequest} sent
+     * to {@link CameraViewfinder} can override the
+     * {@link androidx.camera.viewfinder.surface.ImplementationMode} once it has set the
+     * {@link androidx.camera.viewfinder.surface.ImplementationMode}.
+     *
+     * @return The {@link androidx.camera.viewfinder.surface.ImplementationMode} for
+     * {@link CameraViewfinder}.
+     */
+    @UiThread
+    @NonNull
+    public androidx.camera.viewfinder.surface.ImplementationMode getSurfaceImplementationMode() {
+        checkUiThread();
         return mImplementationMode;
     }
 
@@ -262,7 +295,7 @@
      * <p> The result is a {@link ListenableFuture} of {@link Surface}, which provides the
      * functionality to attach listeners and propagate exceptions.
      *
-     * <pre>
+     * <pre>{@code
      * ViewfinderSurfaceRequest request = new ViewfinderSurfaceRequest(
      *     new Size(width, height), cameraManager.getCameraCharacteristics(cameraId));
      *
@@ -280,22 +313,73 @@
      *     {@literal @}Override
      *     public void onFailure(Throwable t) {}
      * }, ContextCompat.getMainExecutor(getContext()));
-     * </pre>
+     * }</pre>
      *
      * @param surfaceRequest The {@link ViewfinderSurfaceRequest} to get a surface.
      * @return The requested surface.
      *
      * @see ViewfinderSurfaceRequest
+     * @deprecated Use
+     * {@link #requestSurfaceAsync(androidx.camera.viewfinder.surface.ViewfinderSurfaceRequest)}
+     * instead. The {@link ViewfinderSurfaceRequest} in camera-viewfinder will be made obsolete
+     * with the introduction of camera-viewfinder-core.
      */
+    @Deprecated
     @UiThread
     @NonNull
     public ListenableFuture<Surface> requestSurfaceAsync(
             @NonNull ViewfinderSurfaceRequest surfaceRequest) {
+        return requestSurfaceAsync(surfaceRequest.getViewfinderSurfaceRequest());
+    }
+
+    /**
+     * Requests surface by sending a
+     * {@link androidx.camera.viewfinder.surface.ViewfinderSurfaceRequest}.
+     *
+     * <p> Only one request can be handled at the same time. If requesting a surface with
+     * the same {@link androidx.camera.viewfinder.surface.ViewfinderSurfaceRequest}, the previous
+     * requested surface will be returned. If requesting a surface with a new
+     * {@link androidx.camera.viewfinder.surface.ViewfinderSurfaceRequest}, the previous
+     * requested surface will be released and a new surface will be requested.
+     *
+     * <p> The result is a {@link ListenableFuture} of {@link Surface}, which provides the
+     * functionality to attach listeners and propagate exceptions.
+     *
+     * <pre>{@code
+     * ViewfinderSurfaceRequest request = new ViewfinderSurfaceRequest(
+     *     new Size(width, height), cameraManager.getCameraCharacteristics(cameraId));
+     *
+     * ListenableFuture<Surface> surfaceListenableFuture =
+     *     mCameraViewFinder.requestSurfaceAsync(request);
+     *
+     * Futures.addCallback(surfaceListenableFuture, new FutureCallback<Surface>() {
+     *     {@literal @}Override
+     *     public void onSuccess({@literal @}Nullable Surface surface) {
+     *         if (surface != null) {
+     *             createCaptureSession(surface);
+     *         }
+     *     }
+     *
+     *     {@literal @}Override
+     *     public void onFailure(Throwable t) {}
+     * }, ContextCompat.getMainExecutor(getContext()));
+     * }</pre>
+     *
+     * @param surfaceRequest The {@link androidx.camera.viewfinder.surface.ViewfinderSurfaceRequest}
+     *                       to get a surface.
+     * @return The requested surface.
+     *
+     * @see androidx.camera.viewfinder.surface.ViewfinderSurfaceRequest
+     */
+    @UiThread
+    @NonNull
+    public ListenableFuture<Surface> requestSurfaceAsync(
+            @NonNull androidx.camera.viewfinder.surface.ViewfinderSurfaceRequest surfaceRequest) {
         checkUiThread();
 
         if (mCurrentSurfaceRequest != null
                 && surfaceRequest.equals(mCurrentSurfaceRequest)) {
-            return mCurrentSurfaceRequest.getViewfinderSurface().getSurface();
+            return mCurrentSurfaceRequest.getSurface().getSurfaceAsync();
         }
 
         if (mCurrentSurfaceRequest != null) {
@@ -303,7 +387,7 @@
         }
 
         ListenableFuture<Surface> surfaceListenableFuture =
-                surfaceRequest.getViewfinderSurface().getSurface();
+                surfaceRequest.getSurface().getSurfaceAsync();
         mCurrentSurfaceRequest = surfaceRequest;
 
         provideSurfaceIfReady();
@@ -362,7 +446,9 @@
     }
 
     @VisibleForTesting
-    static boolean shouldUseTextureView(@NonNull final ImplementationMode implementationMode) {
+    static boolean shouldUseTextureView(
+            @NonNull final androidx.camera.viewfinder.surface.ImplementationMode implementationMode
+    ) {
         boolean hasSurfaceViewQuirk = DeviceQuirks.get(SurfaceViewStretchedQuirk.class) != null
                 ||  DeviceQuirks.get(SurfaceViewNotCroppedByParentQuirk.class) != null;
         if (Build.VERSION.SDK_INT <= 24 || hasSurfaceViewQuirk) {
@@ -392,7 +478,8 @@
     }
 
     private boolean provideSurfaceIfReady() {
-        final ViewfinderSurfaceRequest surfaceRequest = mCurrentSurfaceRequest;
+        final androidx.camera.viewfinder.surface.ViewfinderSurfaceRequest surfaceRequest =
+                mCurrentSurfaceRequest;
         final ViewfinderSurfaceProvider surfaceProvider = mSurfaceProvider;
         if (surfaceProvider != null && surfaceRequest != null) {
             surfaceProvider.onSurfaceRequested(surfaceRequest);
@@ -428,7 +515,10 @@
      * {@link TextureView} is better supported by a wider range of devices. The option is used by
      * {@link CameraViewfinder} to decide what is the best internal implementation given the device
      * capabilities and user configurations.
+     *
+     * @deprecated Use {@link androidx.camera.viewfinder.surface.ImplementationMode} instead.
      */
+    @Deprecated
     @RequiresApi(21) // TODO(b/200306659): Remove and replace with annotation on package-info.java
     public enum ImplementationMode {
 
@@ -603,7 +693,8 @@
         public void onDisplayChanged(int displayId) {
             Display display = getDisplay();
             if (display != null && display.getDisplayId() == displayId) {
-                ViewfinderSurfaceRequest surfaceRequest = mCurrentSurfaceRequest;
+                androidx.camera.viewfinder.surface.ViewfinderSurfaceRequest surfaceRequest =
+                        mCurrentSurfaceRequest;
                 if (surfaceRequest != null) {
                     mViewfinderTransformation.updateTransformInfo(
                             createTransformInfo(surfaceRequest.getResolution(),
diff --git a/camera/camera-viewfinder/src/main/java/androidx/camera/viewfinder/CameraViewfinderExt.kt b/camera/camera-viewfinder/src/main/java/androidx/camera/viewfinder/CameraViewfinderExt.kt
index 1193fc1..f5dfce2 100644
--- a/camera/camera-viewfinder/src/main/java/androidx/camera/viewfinder/CameraViewfinderExt.kt
+++ b/camera/camera-viewfinder/src/main/java/androidx/camera/viewfinder/CameraViewfinderExt.kt
@@ -22,11 +22,21 @@
 
 /**
  * Provides a suspending function of [CameraViewfinder.requestSurfaceAsync] to request
- * a [Surface] by sending a [ViewfinderSurfaceRequest].
+ * a [Surface] by sending a [androidx.camera.viewfinder.surface.ViewfinderSurfaceRequest].
  */
 @RequiresApi(21) // TODO(b/200306659): Remove and replace with annotation on package-info.java
 object CameraViewfinderExt {
+
+    @Suppress("DEPRECATION")
+    @Deprecated(message = "Use androidx.camera.viewfinder.surface.ViewfinderSurfaceRequest " +
+        "as argument",
+        replaceWith = ReplaceWith("requestSurface using " +
+            "androidx.camera.viewfinder.surface.ViewfinderSurfaceRequest"))
     suspend fun CameraViewfinder.requestSurface(
         viewfinderSurfaceRequest: ViewfinderSurfaceRequest
     ): Surface = requestSurfaceAsync(viewfinderSurfaceRequest).await()
+
+    suspend fun CameraViewfinder.requestSurface(
+        viewfinderSurfaceRequest: androidx.camera.viewfinder.surface.ViewfinderSurfaceRequest
+    ): Surface = requestSurfaceAsync(viewfinderSurfaceRequest).await()
 }
diff --git a/camera/camera-viewfinder/src/main/java/androidx/camera/viewfinder/SurfaceViewImplementation.java b/camera/camera-viewfinder/src/main/java/androidx/camera/viewfinder/SurfaceViewImplementation.java
index 9b56178..986b388 100644
--- a/camera/camera-viewfinder/src/main/java/androidx/camera/viewfinder/SurfaceViewImplementation.java
+++ b/camera/camera-viewfinder/src/main/java/androidx/camera/viewfinder/SurfaceViewImplementation.java
@@ -32,6 +32,7 @@
 import androidx.annotation.RequiresApi;
 import androidx.annotation.UiThread;
 import androidx.camera.viewfinder.internal.utils.Logger;
+import androidx.camera.viewfinder.surface.ViewfinderSurfaceRequest;
 import androidx.core.content.ContextCompat;
 import androidx.core.util.Preconditions;
 
@@ -254,7 +255,7 @@
         private void invalidateSurface() {
             if (mSurfaceRequest != null) {
                 Logger.d(TAG, "Surface invalidated " + mSurfaceRequest);
-                mSurfaceRequest.getViewfinderSurface().close();
+                mSurfaceRequest.getSurface().close();
             }
         }
     }
diff --git a/camera/camera-viewfinder/src/main/java/androidx/camera/viewfinder/TextureViewImplementation.java b/camera/camera-viewfinder/src/main/java/androidx/camera/viewfinder/TextureViewImplementation.java
index e45a70d..730966f 100644
--- a/camera/camera-viewfinder/src/main/java/androidx/camera/viewfinder/TextureViewImplementation.java
+++ b/camera/camera-viewfinder/src/main/java/androidx/camera/viewfinder/TextureViewImplementation.java
@@ -29,8 +29,9 @@
 import androidx.camera.impl.utils.executor.CameraExecutors;
 import androidx.camera.impl.utils.futures.FutureCallback;
 import androidx.camera.impl.utils.futures.Futures;
-import androidx.camera.viewfinder.ViewfinderSurfaceRequest.Result;
 import androidx.camera.viewfinder.internal.utils.Logger;
+import androidx.camera.viewfinder.surface.ViewfinderSurfaceRequest;
+import androidx.camera.viewfinder.surface.ViewfinderSurfaceRequest.Result;
 import androidx.concurrent.futures.CallbackToFutureAdapter;
 import androidx.core.content.ContextCompat;
 import androidx.core.util.Consumer;
@@ -82,6 +83,7 @@
         mTextureView.setLayoutParams(
                 new FrameLayout.LayoutParams(mResolution.getWidth(), mResolution.getHeight()));
         mTextureView.setSurfaceTextureListener(new TextureView.SurfaceTextureListener() {
+            @SuppressWarnings("ObjectToString")
             @Override
             public void onSurfaceTextureAvailable(@NonNull final SurfaceTexture surfaceTexture,
                     final int width, final int height) {
@@ -95,7 +97,7 @@
                 if (mSurfaceReleaseFuture != null && mSurfaceRequest != null) {
                     Preconditions.checkNotNull(mSurfaceRequest);
                     Logger.d(TAG, "Surface invalidated " + mSurfaceRequest);
-                    mSurfaceRequest.getViewfinderSurface().close();
+                    mSurfaceRequest.getSurface().close();
                 } else {
                     tryToProvideViewfinderSurface();
                 }
@@ -119,7 +121,7 @@
                             new FutureCallback<Result>() {
                                 @Override
                                 public void onSuccess(Result result) {
-                                    Preconditions.checkState(result.getResultCode()
+                                    Preconditions.checkState(result.getCode()
                                                     != Result.RESULT_SURFACE_ALREADY_PROVIDED,
                                             "Unexpected result from SurfaceRequest. Surface was "
                                                     + "provided twice.");
diff --git a/camera/camera-viewfinder/src/main/java/androidx/camera/viewfinder/ViewfinderImplementation.java b/camera/camera-viewfinder/src/main/java/androidx/camera/viewfinder/ViewfinderImplementation.java
index 326cedc..e78aff8 100644
--- a/camera/camera-viewfinder/src/main/java/androidx/camera/viewfinder/ViewfinderImplementation.java
+++ b/camera/camera-viewfinder/src/main/java/androidx/camera/viewfinder/ViewfinderImplementation.java
@@ -24,6 +24,7 @@
 import androidx.annotation.NonNull;
 import androidx.annotation.Nullable;
 import androidx.annotation.RequiresApi;
+import androidx.camera.viewfinder.surface.ViewfinderSurfaceRequest;
 
 /**
  * Wraps the underlying handling of the {@link android.view.Surface} used for viewfinder, which is
diff --git a/camera/camera-viewfinder/src/main/java/androidx/camera/viewfinder/ViewfinderSurfaceRequest.java b/camera/camera-viewfinder/src/main/java/androidx/camera/viewfinder/ViewfinderSurfaceRequest.java
index 9eace4a..7ee6d90 100644
--- a/camera/camera-viewfinder/src/main/java/androidx/camera/viewfinder/ViewfinderSurfaceRequest.java
+++ b/camera/camera-viewfinder/src/main/java/androidx/camera/viewfinder/ViewfinderSurfaceRequest.java
@@ -33,27 +33,13 @@
 import androidx.annotation.NonNull;
 import androidx.annotation.Nullable;
 import androidx.annotation.RequiresApi;
-import androidx.annotation.RestrictTo;
-import androidx.annotation.RestrictTo.Scope;
-import androidx.camera.impl.utils.executor.CameraExecutors;
-import androidx.camera.impl.utils.futures.FutureCallback;
-import androidx.camera.impl.utils.futures.Futures;
-import androidx.camera.viewfinder.CameraViewfinder.ImplementationMode;
-import androidx.camera.viewfinder.internal.surface.ViewfinderSurface;
-import androidx.camera.viewfinder.internal.utils.Logger;
-import androidx.concurrent.futures.CallbackToFutureAdapter;
-import androidx.core.util.Consumer;
-import androidx.core.util.Preconditions;
+import androidx.camera.viewfinder.impl.surface.DeferredSurface;
 
-import com.google.auto.value.AutoValue;
 import com.google.common.util.concurrent.ListenableFuture;
 
 import java.lang.annotation.Retention;
 import java.lang.annotation.RetentionPolicy;
-import java.util.concurrent.CancellationException;
-import java.util.concurrent.ExecutionException;
 import java.util.concurrent.Executor;
-import java.util.concurrent.atomic.AtomicReference;
 
 /**
  * The request to get a {@link Surface} to display camera feed.
@@ -67,173 +53,33 @@
  *
  * <p> Calling {@link ViewfinderSurfaceRequest#markSurfaceSafeToRelease()} will notify the
  * surface provider that the surface is not needed and related resources can be released.
+ *
+ * @deprecated Use {@link androidx.camera.viewfinder.surface.ViewfinderSurfaceRequest} instead.
  */
+@Deprecated
 @RequiresApi(21) // TODO(b/200306659): Remove and replace with annotation on package-info.java
 public class ViewfinderSurfaceRequest {
 
     private static final String TAG = "ViewfinderSurfaceRequest";
 
-    @NonNull private final Size mResolution;
-    @NonNull private final ViewfinderSurface mInternalViewfinderSurface;
-    @NonNull private final CallbackToFutureAdapter.Completer<Void> mRequestCancellationCompleter;
-    @NonNull private final ListenableFuture<Void> mSessionStatusFuture;
-    @NonNull private final CallbackToFutureAdapter.Completer<Surface> mSurfaceCompleter;
-    @LensFacingValue private int mLensFacing;
-    @SensorOrientationDegreesValue private int mSensorOrientation;
-    @Nullable
-    private ImplementationMode mImplementationMode;
-    @SuppressWarnings("WeakerAccess") /*synthetic accessor */
-    @NonNull
-    final ListenableFuture<Surface> mSurfaceFuture;
+    @NonNull private androidx.camera.viewfinder.surface.ViewfinderSurfaceRequest
+            mViewfinderSurfaceRequest;
 
     /**
      * Creates a new surface request with surface resolution, camera device, lens facing and
      * sensor orientation information.
      *
-     * @param resolution The requested surface resolution. It is the output surface size
-     *                   the camera is configured with, instead of {@link CameraViewfinder}
-     *                   view size.
-     * @param lensFacing The camera lens facing.
-     * @param sensorOrientation THe camera sensor orientation.
-     * @param implementationMode The {@link ImplementationMode} to apply to the viewfinder.
+     * surfaceRequest The {@link androidx.camera.viewfinder.surface.ViewfinderSurfaceRequest}.
      */
     ViewfinderSurfaceRequest(
-            @NonNull Size resolution,
-            @LensFacingValue int lensFacing,
-            @SensorOrientationDegreesValue int sensorOrientation,
-            @Nullable ImplementationMode implementationMode) {
-        mResolution = resolution;
-        mLensFacing = lensFacing;
-        mSensorOrientation = sensorOrientation;
-        mImplementationMode = implementationMode;
-
-        // To ensure concurrency and ordering, operations are chained. Completion can only be
-        // triggered externally by the top-level completer (mSurfaceCompleter). The other future
-        // completers are only completed by callbacks set up within the constructor of this class
-        // to ensure correct ordering of events.
-
-        // Cancellation listener must be called last to ensure the result can be retrieved from
-        // the session listener.
-        String surfaceRequestString =
-                "SurfaceRequest[size: " + resolution + ", id: " + this.hashCode() + "]";
-        AtomicReference<CallbackToFutureAdapter.Completer<Void>> cancellationCompleterRef =
-                new AtomicReference<>(null);
-        ListenableFuture<Void> requestCancellationFuture =
-                CallbackToFutureAdapter.getFuture(completer -> {
-                    cancellationCompleterRef.set(completer);
-                    return surfaceRequestString + "-cancellation";
-                });
-        CallbackToFutureAdapter.Completer<Void> requestCancellationCompleter =
-                Preconditions.checkNotNull(cancellationCompleterRef.get());
-        mRequestCancellationCompleter = requestCancellationCompleter;
-
-        // Surface session status future completes and is responsible for finishing the
-        // cancellation listener.
-        AtomicReference<CallbackToFutureAdapter.Completer<Void>> sessionStatusCompleterRef =
-                new AtomicReference<>(null);
-        mSessionStatusFuture = CallbackToFutureAdapter.getFuture(completer -> {
-            sessionStatusCompleterRef.set(completer);
-            return surfaceRequestString + "-status";
-        });
-
-        Futures.addCallback(mSessionStatusFuture, new FutureCallback<Void>() {
-            @Override
-            public void onSuccess(@Nullable Void result) {
-                // Cancellation didn't occur, so complete the cancellation future. There
-                // shouldn't ever be any standard listeners on this future, so nothing should be
-                // invoked.
-                Preconditions.checkState(requestCancellationCompleter.set(null));
-            }
-
-            @Override
-            public void onFailure(@NonNull Throwable t) {
-                if (t instanceof RequestCancelledException) {
-                    // Cancellation occurred. Notify listeners.
-                    Preconditions.checkState(requestCancellationFuture.cancel(false));
-                } else {
-                    // Cancellation didn't occur, complete the future so cancellation listeners
-                    // are not invoked.
-                    Preconditions.checkState(requestCancellationCompleter.set(null));
-                }
-            }
-        }, CameraExecutors.directExecutor());
-
-        // Create the surface future/completer. This will be used to complete the rest of the
-        // future chain and can be set externally via SurfaceRequest methods.
-        CallbackToFutureAdapter.Completer<Void> sessionStatusCompleter =
-                Preconditions.checkNotNull(sessionStatusCompleterRef.get());
-        AtomicReference<CallbackToFutureAdapter.Completer<Surface>> surfaceCompleterRef =
-                new AtomicReference<>(null);
-        mSurfaceFuture = CallbackToFutureAdapter.getFuture(completer -> {
-            surfaceCompleterRef.set(completer);
-            return surfaceRequestString + "-Surface";
-        });
-        mSurfaceCompleter = Preconditions.checkNotNull(surfaceCompleterRef.get());
-
-        // Create the viewfinder surface which will be used for communicating when the
-        // camera and consumer are done using the surface. Note this anonymous inner class holds
-        // an implicit reference to the ViewfinderSurfaceRequest. This is by design, and ensures the
-        // ViewfinderSurfaceRequest and all contained future completers will not be garbage
-        // collected as long as the ViewfinderSurface is referenced externally (via
-        // getViewfinderSurface()).
-        mInternalViewfinderSurface = new ViewfinderSurface() {
-            @NonNull
-            @Override
-            protected ListenableFuture<Surface> provideSurfaceAsync() {
-                Logger.d(TAG,
-                        "mInternalViewfinderSurface + " + mInternalViewfinderSurface + " "
-                                + "provideSurface");
-                return mSurfaceFuture;
-            }
-        };
-        ListenableFuture<Void> terminationFuture =
-                mInternalViewfinderSurface.getTerminationFuture();
-
-        // Propagate surface completion to the session future.
-        Futures.addCallback(mSurfaceFuture, new FutureCallback<Surface>() {
-            @Override
-            public void onSuccess(@Nullable Surface surface) {
-                // On successful setting of a surface, defer completion of the session future to
-                // the ViewfinderSurface termination future. Once that future completes, then it
-                // is safe to release the Surface and associated resources.
-
-                Futures.propagate(terminationFuture, sessionStatusCompleter);
-            }
-
-            @Override
-            public void onFailure(@NonNull Throwable t) {
-                // Translate cancellation into a SurfaceRequestCancelledException. Other
-                // exceptions mean either the request was completed via willNotProvideSurface() or a
-                // programming error occurred. In either case, the user will never see the
-                // session future (an immediate future will be returned instead), so complete the
-                // future so cancellation listeners are never called.
-                if (t instanceof CancellationException) {
-                    Preconditions.checkState(sessionStatusCompleter.setException(
-                            new RequestCancelledException(
-                                    surfaceRequestString + " cancelled.", t)));
-                } else {
-                    sessionStatusCompleter.set(null);
-                }
-            }
-        }, CameraExecutors.directExecutor());
-
-        // If the viewfinder surface is terminated, there are two cases:
-        // 1. The surface has not yet been provided to the camera (or marked as 'will not
-        //    complete'). Treat this as if the surface request has been cancelled.
-        // 2. The surface was already provided to the camera. In this case the camera is now
-        //    finished with the surface, so cancelling the surface future below will be a no-op.
-        terminationFuture.addListener(() -> {
-            Logger.d(TAG,
-                    "mInternalViewfinderSurface + " + mInternalViewfinderSurface + " "
-                            + "terminateFuture triggered");
-            mSurfaceFuture.cancel(true);
-        }, CameraExecutors.directExecutor());
+            @NonNull androidx.camera.viewfinder.surface.ViewfinderSurfaceRequest surfaceRequest) {
+        mViewfinderSurfaceRequest = surfaceRequest;
     }
 
     @Override
     @SuppressWarnings("GenericException") // super.finalize() throws Throwable
     protected void finalize() throws Throwable {
-        mInternalViewfinderSurface.close();
+        mViewfinderSurfaceRequest.getSurface().close();
         super.finalize();
     }
 
@@ -250,7 +96,7 @@
      */
     @NonNull
     public Size getResolution() {
-        return mResolution;
+        return mViewfinderSurfaceRequest.getResolution();
     }
 
     /**
@@ -263,7 +109,7 @@
      */
     @SensorOrientationDegreesValue
     public int getSensorOrientation() {
-        return mSensorOrientation;
+        return mViewfinderSurfaceRequest.getSensorOrientation();
     }
 
     /**
@@ -276,20 +122,30 @@
      */
     @LensFacingValue
     public int getLensFacing() {
-        return mLensFacing;
+        return mViewfinderSurfaceRequest.getLensFacing();
     }
 
     /**
-     * Returns the {@link ImplementationMode}.
+     * Returns the {@link androidx.camera.viewfinder.CameraViewfinder.ImplementationMode}.
      *
-     * <p>The value is set by {@link Builder#setImplementationMode(ImplementationMode)}.
+     * <p>The value is set by {@link
+     * Builder#setImplementationMode(androidx.camera.viewfinder.CameraViewfinder.ImplementationMode)
+     * }.
      *
-     * @return {@link ImplementationMode}. The value will be null if it's not set via
-     * {@link Builder#setImplementationMode(ImplementationMode)}.
+     * @return {@link androidx.camera.viewfinder.CameraViewfinder.ImplementationMode}.
+     *         The value will be null if it's not set via
+     * {@link Builder#setImplementationMode(
+     * androidx.camera.viewfinder.CameraViewfinder.ImplementationMode)}.
      */
     @Nullable
-    public ImplementationMode getImplementationMode() {
-        return mImplementationMode;
+    public androidx.camera.viewfinder.CameraViewfinder.ImplementationMode getImplementationMode() {
+        return androidx.camera.viewfinder.CameraViewfinder.ImplementationMode.fromId(
+                mViewfinderSurfaceRequest.getImplementationMode().getId());
+    }
+
+    @NonNull
+    androidx.camera.viewfinder.surface.ViewfinderSurfaceRequest getViewfinderSurfaceRequest() {
+        return mViewfinderSurfaceRequest;
     }
 
     /**
@@ -299,125 +155,30 @@
      * related resources can be released.
      */
     public void markSurfaceSafeToRelease() {
-        mInternalViewfinderSurface.close();
+        mViewfinderSurfaceRequest.markSurfaceSafeToRelease();
     }
 
     @NonNull
-    ViewfinderSurface getViewfinderSurface() {
-        return mInternalViewfinderSurface;
+    DeferredSurface getViewfinderSurface() {
+        return mViewfinderSurfaceRequest.getSurface();
     }
 
     @SuppressLint("PairedRegistration")
     void addRequestCancellationListener(@NonNull Executor executor,
             @NonNull Runnable listener) {
-        mRequestCancellationCompleter.addCancellationListener(listener, executor);
-    }
-
-    /**
-     * Completes the request for a {@link Surface} if it has not already been
-     * completed or cancelled.
-     *
-     * <p>Once the camera no longer needs the provided surface, the {@code resultListener} will be
-     * invoked with a {@link Result} containing {@link Result#RESULT_SURFACE_USED_SUCCESSFULLY}.
-     * At this point it is safe to release the surface and any underlying resources. Releasing
-     * the surface before receiving this signal may cause undesired behavior on lower API levels.
-     *
-     * <p>If the request is cancelled by the camera before successfully attaching the
-     * provided surface to the camera, then the {@code resultListener} will be invoked with a
-     * {@link Result} containing {@link Result#RESULT_REQUEST_CANCELLED}.
-     *
-     * <p>If the request was previously completed via {@link #willNotProvideSurface()}, then
-     * {@code resultListener} will be invoked with a {@link Result} containing
-     * {@link Result#RESULT_WILL_NOT_PROVIDE_SURFACE}.
-     *
-     * <p>Upon returning from this method, the surface request is guaranteed to be complete.
-     * However, only the {@code resultListener} provided to the first invocation of this method
-     * should be used to track when the provided {@link Surface} is no longer in use by the
-     * camera, as subsequent invocations will always invoke the {@code resultListener} with a
-     * {@link Result} containing {@link Result#RESULT_SURFACE_ALREADY_PROVIDED}.
-     *
-     * @param surface        The surface which will complete the request.
-     * @param executor       Executor used to execute the {@code resultListener}.
-     * @param resultListener Listener used to track how the surface is used by the camera in
-     *                       response to being provided by this method.
-     *
-     */
-    void provideSurface(
-            @NonNull Surface surface,
-            @NonNull Executor executor,
-            @NonNull Consumer<Result> resultListener) {
-        if (mSurfaceCompleter.set(surface) || mSurfaceFuture.isCancelled()) {
-            // Session will be pending completion (or surface request was cancelled). Return the
-            // session future.
-            Futures.addCallback(mSessionStatusFuture, new FutureCallback<Void>() {
-                @Override
-                public void onSuccess(@Nullable Void result) {
-                    resultListener.accept(Result.of(Result.RESULT_SURFACE_USED_SUCCESSFULLY,
-                            surface));
-                }
-
-                @Override
-                public void onFailure(@NonNull Throwable t) {
-                    Preconditions.checkState(t instanceof RequestCancelledException, "Camera "
-                            + "surface session should only fail with request "
-                            + "cancellation. Instead failed due to:\n" + t);
-                    resultListener.accept(Result.of(Result.RESULT_REQUEST_CANCELLED, surface));
-                }
-            }, executor);
-        } else {
-            // Surface request is already complete
-            Preconditions.checkState(mSurfaceFuture.isDone());
-            try {
-                mSurfaceFuture.get();
-                // Getting this far means the surface was already provided.
-                executor.execute(
-                        () -> resultListener.accept(
-                                Result.of(Result.RESULT_SURFACE_ALREADY_PROVIDED, surface)));
-            } catch (InterruptedException | ExecutionException e) {
-                executor.execute(
-                        () -> resultListener.accept(
-                                Result.of(Result.RESULT_WILL_NOT_PROVIDE_SURFACE, surface)));
-            }
-        }
-    }
-
-    /**
-     * Signals that the request will never be fulfilled.
-     *
-     * <p>This may be called in the case where the application may be shutting down and a
-     * surface will never be produced to fulfill the request.
-     *
-     * <p>This will be called by CameraViewfinder as soon as it is known that the request will not
-     * be fulfilled. Failure to complete the SurfaceRequest via {@code willNotProvideSurface()}
-     * or {@link #provideSurface(Surface, Executor, Consumer)} may cause long delays in shutting
-     * down the camera.
-     *
-     * <p>Upon returning from this method, the request is guaranteed to be complete, regardless
-     * of the return value. If the request was previously successfully completed by
-     * {@link #provideSurface(Surface, Executor, Consumer)}, invoking this method will return
-     * {@code false}, and will have no effect on how the surface is used by the camera.
-     *
-     * @return {@code true} if this call to {@code willNotProvideSurface()} successfully
-     * completes the request, i.e., the request has not already been completed via
-     * {@link #provideSurface(Surface, Executor, Consumer)} or by a previous call to
-     * {@code willNotProvideSurface()} and has not already been cancelled by the camera.
-     *
-     */
-    boolean willNotProvideSurface() {
-        return mSurfaceCompleter.setException(
-                new ViewfinderSurface.SurfaceUnavailableException("Surface request "
-                        + "will not complete."));
+        mViewfinderSurfaceRequest.addRequestCancellationListener(executor, listener);
     }
 
     /**
      * Builder for {@link ViewfinderSurfaceRequest}.
+     *
+     * @deprecated Use {@link androidx.camera.viewfinder.surface.ViewfinderSurfaceRequest.Builder}
+     * instead.
      */
+    @Deprecated
     public static final class Builder {
-
-        @NonNull private final Size mResolution;
-        @LensFacingValue private int mLensFacing = LENS_FACING_BACK;
-        @SensorOrientationDegreesValue private int mSensorOrientation = 0;
-        @Nullable private ImplementationMode mImplementationMode;
+        @NonNull
+        private androidx.camera.viewfinder.surface.ViewfinderSurfaceRequest.Builder mBuilder;
 
         /**
          * Constructor for {@link Builder}.
@@ -427,7 +188,8 @@
          * @param resolution viewfinder resolution.
          */
         public Builder(@NonNull Size resolution) {
-            mResolution = resolution;
+            mBuilder = new androidx.camera.viewfinder.surface.ViewfinderSurfaceRequest.Builder(
+                    resolution);
         }
 
         /**
@@ -439,10 +201,7 @@
          * @param builder {@link Builder} instance.
          */
         public Builder(@NonNull Builder builder) {
-            mResolution = builder.mResolution;
-            mImplementationMode = builder.mImplementationMode;
-            mLensFacing = builder.mLensFacing;
-            mSensorOrientation = builder.mSensorOrientation;
+            mBuilder = builder.mBuilder;
         }
 
         /**
@@ -455,32 +214,46 @@
          * @param surfaceRequest {@link ViewfinderSurfaceRequest} instance.
          */
         public Builder(@NonNull ViewfinderSurfaceRequest surfaceRequest) {
-            mResolution = surfaceRequest.getResolution();
-            mImplementationMode = surfaceRequest.getImplementationMode();
-            mLensFacing = surfaceRequest.getLensFacing();
-            mSensorOrientation = surfaceRequest.getSensorOrientation();
+            mBuilder = new androidx.camera.viewfinder.surface.ViewfinderSurfaceRequest.Builder(
+                    surfaceRequest.getResolution());
+            mBuilder.setSensorOrientation(surfaceRequest.getSensorOrientation());
+            mBuilder.setLensFacing(surfaceRequest.getLensFacing());
+            mBuilder.setImplementationMode(
+                    androidx.camera.viewfinder.surface.ImplementationMode.fromId(
+                            surfaceRequest.getImplementationMode().getId()));
         }
 
         /**
-         * Sets the {@link ImplementationMode}.
+         * Sets the {@link androidx.camera.viewfinder.CameraViewfinder.ImplementationMode}.
          *
          * <p><b>Possible values:</b></p>
          * <ul>
-         *   <li>{@link ImplementationMode#PERFORMANCE PERFORMANCE}</li>
-         *   <li>{@link ImplementationMode#COMPATIBLE COMPATIBLE}</li>
+         *   <li>{@link
+         *   androidx.camera.viewfinder.CameraViewfinder.ImplementationMode#PERFORMANCE PERFORMANCE}
+         *   </li>
+         *   <li>{@link
+         *   androidx.camera.viewfinder.CameraViewfinder.ImplementationMode#COMPATIBLE COMPATIBLE}
+         *   </li>
          * </ul>
          *
-         * <p>If not set or setting to null, the {@link ImplementationMode} set via {@code app
+         * <p>If not set or setting to null, the
+         * {@link androidx.camera.viewfinder.CameraViewfinder.ImplementationMode} set via {@code app
          * :implementationMode} in layout xml will be used for {@link CameraViewfinder}. If not
-         * set in the layout xml, the default value {@link ImplementationMode#PERFORMANCE} will
+         * set in the layout xml, the default value
+         * {@link androidx.camera.viewfinder.CameraViewfinder.ImplementationMode#PERFORMANCE} will
          * be used in {@link CameraViewfinder}.
          *
-         * @param implementationMode The {@link ImplementationMode}.
+         * @param implementationMode The {@link
+         * androidx.camera.viewfinder.CameraViewfinder.ImplementationMode}.
          * @return This builder.
          */
         @NonNull
-        public Builder setImplementationMode(@Nullable ImplementationMode implementationMode) {
-            mImplementationMode = implementationMode;
+        public Builder setImplementationMode(
+                @Nullable
+                androidx.camera.viewfinder.CameraViewfinder.ImplementationMode implementationMode) {
+            mBuilder.setImplementationMode(
+                    androidx.camera.viewfinder.surface.ImplementationMode.fromId(
+                            implementationMode.getId()));
             return this;
         }
 
@@ -503,7 +276,7 @@
          */
         @NonNull
         public Builder setLensFacing(@LensFacingValue int lensFacing) {
-            mLensFacing = lensFacing;
+            mBuilder.setLensFacing(lensFacing);
             return this;
         }
 
@@ -522,7 +295,7 @@
          */
         @NonNull
         public Builder setSensorOrientation(@SensorOrientationDegreesValue int sensorOrientation) {
-            mSensorOrientation = sensorOrientation;
+            mBuilder.setSensorOrientation(sensorOrientation);
             return this;
         }
 
@@ -532,153 +305,7 @@
          */
         @NonNull
         public ViewfinderSurfaceRequest build() {
-            if (mLensFacing != LENS_FACING_FRONT
-                    && mLensFacing != LENS_FACING_BACK
-                    && mLensFacing != LENS_FACING_EXTERNAL) {
-                throw new IllegalArgumentException("Lens facing value: " + mLensFacing + " is "
-                        + "invalid");
-            }
-
-            if (mSensorOrientation != 0
-                    && mSensorOrientation != 90
-                    && mSensorOrientation != 180
-                    && mSensorOrientation != 270) {
-                throw new IllegalArgumentException("Sensor orientation value: "
-                        + mSensorOrientation + " is invalid");
-            }
-
-            return new ViewfinderSurfaceRequest(
-                    mResolution,
-                    mLensFacing,
-                    mSensorOrientation,
-                    mImplementationMode);
-        }
-    }
-
-    static final class RequestCancelledException extends RuntimeException {
-        RequestCancelledException(@NonNull String message, @NonNull Throwable cause) {
-            super(message, cause);
-        }
-    }
-
-    /**
-     * Result of providing a surface to a {@link ViewfinderSurfaceRequest} via
-     * {@link #provideSurface(Surface, Executor, Consumer)}.
-     *
-     */
-    @AutoValue
-    abstract static class Result {
-
-        /**
-         * Possible result codes.
-         *
-         */
-        @IntDef({RESULT_SURFACE_USED_SUCCESSFULLY, RESULT_REQUEST_CANCELLED, RESULT_INVALID_SURFACE,
-                RESULT_SURFACE_ALREADY_PROVIDED, RESULT_WILL_NOT_PROVIDE_SURFACE})
-        @Retention(RetentionPolicy.SOURCE)
-        @RestrictTo(Scope.LIBRARY)
-        public @interface ResultCode {
-        }
-
-        /**
-         * Provided surface was successfully used by the camera and eventually detached once no
-         * longer needed by the camera.
-         *
-         * <p>This result denotes that it is safe to release the {@link Surface} and any underlying
-         * resources.
-         *
-         * <p>For compatibility reasons, the {@link Surface} object should not be reused by
-         * future {@link ViewfinderSurfaceRequest SurfaceRequests}, and a new surface should be
-         * created instead.
-         */
-        public static final int RESULT_SURFACE_USED_SUCCESSFULLY = 0;
-
-        /**
-         * Provided surface was never attached to the camera due to the
-         * {@link ViewfinderSurfaceRequest} being cancelled by the camera.
-         *
-         * <p>It is safe to release or reuse {@link Surface}, assuming it was not previously
-         * attached to a camera via {@link #provideSurface(Surface, Executor, Consumer)}. If
-         * reusing the surface for a future surface request, it should be verified that the
-         * surface still matches the resolution specified by
-         * {@link ViewfinderSurfaceRequest#getResolution()}.
-         */
-        public static final int RESULT_REQUEST_CANCELLED = 1;
-
-        /**
-         * Provided surface could not be used by the camera.
-         *
-         * <p>This is likely due to the {@link Surface} being closed prematurely or the resolution
-         * of the surface not matching the resolution specified by
-         * {@link ViewfinderSurfaceRequest#getResolution()}.
-         */
-        public static final int RESULT_INVALID_SURFACE = 2;
-
-        /**
-         * Surface was not attached to the camera through this invocation of
-         * {@link #provideSurface(Surface, Executor, Consumer)} due to the
-         * {@link ViewfinderSurfaceRequest} already being complete with a surface.
-         *
-         * <p>The {@link ViewfinderSurfaceRequest} has already been completed by a previous
-         * invocation of {@link #provideSurface(Surface, Executor, Consumer)}.
-         *
-         * <p>It is safe to release or reuse the {@link Surface}, assuming it was not previously
-         * attached to a camera via {@link #provideSurface(Surface, Executor, Consumer)}.
-         */
-        public static final int RESULT_SURFACE_ALREADY_PROVIDED = 3;
-
-        /**
-         * Surface was not attached to the camera through this invocation of
-         * {@link #provideSurface(Surface, Executor, Consumer)} due to the
-         * {@link ViewfinderSurfaceRequest} already being marked as "will not provide surface".
-         *
-         * <p>The {@link ViewfinderSurfaceRequest} has already been marked as 'will not provide
-         * surface' by a previous invocation of {@link #willNotProvideSurface()}.
-         *
-         * <p>It is safe to release or reuse the {@link Surface}, assuming it was not previously
-         * attached to a camera via {@link #provideSurface(Surface, Executor, Consumer)}.
-         */
-        public static final int RESULT_WILL_NOT_PROVIDE_SURFACE = 4;
-
-        /**
-         * Creates a result from the given result code and surface.
-         *
-         * <p>Can be used to compare to results returned to {@code resultListener} in
-         * {@link #provideSurface(Surface, Executor, Consumer)}.
-         *
-         * @param code    One of {@link #RESULT_SURFACE_USED_SUCCESSFULLY},
-         *                {@link #RESULT_REQUEST_CANCELLED}, {@link #RESULT_INVALID_SURFACE},
-         *                {@link #RESULT_SURFACE_ALREADY_PROVIDED}, or
-         *                {@link #RESULT_WILL_NOT_PROVIDE_SURFACE}.
-         * @param surface The {@link Surface} used to complete the {@link ViewfinderSurfaceRequest}.
-         */
-        @NonNull
-        static Result of(@ResultCode int code, @NonNull Surface surface) {
-            return new AutoValue_ViewfinderSurfaceRequest_Result(code, surface);
-        }
-
-        /**
-         * Returns the result of invoking {@link #provideSurface(Surface, Executor, Consumer)}
-         * with the surface from {@link #getSurface()}.
-         *
-         * @return One of {@link #RESULT_SURFACE_USED_SUCCESSFULLY},
-         * {@link #RESULT_REQUEST_CANCELLED}, {@link #RESULT_INVALID_SURFACE}, or
-         * {@link #RESULT_SURFACE_ALREADY_PROVIDED}, {@link #RESULT_WILL_NOT_PROVIDE_SURFACE}.
-         */
-        @ResultCode
-        public abstract int getResultCode();
-
-        /**
-         * The surface used to complete a {@link ViewfinderSurfaceRequest} with
-         * {@link #provideSurface(Surface, Executor, Consumer)}.
-         *
-         * @return the surface.
-         */
-        @NonNull
-        public abstract Surface getSurface();
-
-        // Ensure Result can't be subclassed outside the package
-        Result() {
+            return new ViewfinderSurfaceRequest(mBuilder.build());
         }
     }
 
diff --git a/camera/camera-viewfinder/src/main/java/androidx/camera/viewfinder/ViewfinderSurfaceRequestExt.kt b/camera/camera-viewfinder/src/main/java/androidx/camera/viewfinder/ViewfinderSurfaceRequestExt.kt
index 1f16434..9283ecc 100644
--- a/camera/camera-viewfinder/src/main/java/androidx/camera/viewfinder/ViewfinderSurfaceRequestExt.kt
+++ b/camera/camera-viewfinder/src/main/java/androidx/camera/viewfinder/ViewfinderSurfaceRequestExt.kt
@@ -13,6 +13,7 @@
  * See the License for the specific language governing permissions and
  * limitations under the License.
  */
+@file:Suppress("DEPRECATION")
 @file:JvmName("ViewfinderSurfaceRequestUtil")
 
 package androidx.camera.viewfinder
@@ -30,6 +31,10 @@
  * sensor orientation and [ImplementationMode]. If the hardware level is legacy,
  * the [ImplementationMode] will be set to [ImplementationMode.COMPATIBLE].
  */
+@Deprecated(message = "Use androidx.camera.viewfinder.surface.ViewfinderSurfaceRequest as argument",
+    replaceWith = ReplaceWith(
+    "populateFromCharacteristics returning " +
+        "androidx.camera.viewfinder.surface.ViewfinderSurfaceRequest.Builder"))
 @SuppressLint("ClassVerificationFailure")
 @RequiresApi(21)
 fun ViewfinderSurfaceRequest.Builder.populateFromCharacteristics(
diff --git a/camera/camera-viewfinder/src/main/java/androidx/camera/viewfinder/internal/surface/ViewfinderSurface.java b/camera/camera-viewfinder/src/main/java/androidx/camera/viewfinder/internal/surface/ViewfinderSurface.java
deleted file mode 100644
index 24423ac..0000000
--- a/camera/camera-viewfinder/src/main/java/androidx/camera/viewfinder/internal/surface/ViewfinderSurface.java
+++ /dev/null
@@ -1,105 +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.viewfinder.internal.surface;
-
-import android.view.Surface;
-import android.view.SurfaceView;
-import android.view.TextureView;
-
-import androidx.annotation.GuardedBy;
-import androidx.annotation.NonNull;
-import androidx.annotation.Nullable;
-import androidx.annotation.RequiresApi;
-import androidx.camera.impl.utils.futures.Futures;
-import androidx.camera.viewfinder.internal.utils.Logger;
-import androidx.concurrent.futures.CallbackToFutureAdapter;
-
-import com.google.common.util.concurrent.ListenableFuture;
-
-/**
- * A class for creating and tracking use of a {@link Surface} in an asynchronous manner.
- */
-@RequiresApi(21) // TODO(b/200306659): Remove and replace with annotation on package-info.java
-public abstract class ViewfinderSurface {
-
-    private static final String TAG = "ViewfinderSurface";
-
-    @NonNull private final Object mLock = new Object();
-    @NonNull private final ListenableFuture<Void> mTerminationFuture;
-
-    @GuardedBy("mLock")
-    private boolean mClosed = false;
-
-    @Nullable
-    @GuardedBy("mLock")
-    private CallbackToFutureAdapter.Completer<Void> mTerminationCompleter;
-
-    public ViewfinderSurface() {
-        mTerminationFuture = CallbackToFutureAdapter.getFuture(completer -> {
-            synchronized (mLock) {
-                mTerminationCompleter = completer;
-            }
-            return "ViewfinderSurface-termination(" + ViewfinderSurface.this + ")";
-        });
-    }
-
-    @NonNull
-    public final ListenableFuture<Surface> getSurface() {
-        return provideSurfaceAsync();
-    }
-
-    @NonNull
-    public ListenableFuture<Void> getTerminationFuture() {
-        return Futures.nonCancellationPropagating(mTerminationFuture);
-    }
-
-    /**
-     * Close the surface.
-     *
-     * <p> After closing, the underlying surface resources can be safely released by
-     * {@link SurfaceView} or {@link TextureView} implementation.
-     */
-    public void close() {
-        CallbackToFutureAdapter.Completer<Void> terminationCompleter = null;
-        synchronized (mLock) {
-            if (!mClosed) {
-                mClosed = true;
-                terminationCompleter = mTerminationCompleter;
-                mTerminationCompleter = null;
-                Logger.d(TAG,
-                        "surface closed,  closed=true " + this);
-            }
-        }
-
-        if (terminationCompleter != null) {
-            terminationCompleter.set(null);
-        }
-    }
-
-    @NonNull
-    protected abstract ListenableFuture<Surface> provideSurfaceAsync();
-
-    /**
-     * The exception that is returned by the ListenableFuture of {@link #getSurface()} if the
-     * deferrable surface is unable to produce a {@link Surface}.
-     */
-    public static final class SurfaceUnavailableException extends Exception {
-        public SurfaceUnavailableException(@NonNull String message) {
-            super(message);
-        }
-    }
-}
diff --git a/camera/camera-viewfinder/src/main/java/androidx/camera/viewfinder/internal/surface/ViewfinderSurfaceProvider.java b/camera/camera-viewfinder/src/main/java/androidx/camera/viewfinder/internal/surface/ViewfinderSurfaceProvider.java
deleted file mode 100644
index e7c5f26..0000000
--- a/camera/camera-viewfinder/src/main/java/androidx/camera/viewfinder/internal/surface/ViewfinderSurfaceProvider.java
+++ /dev/null
@@ -1,45 +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.viewfinder.internal.surface;
-
-import android.view.Surface;
-
-import androidx.annotation.NonNull;
-import androidx.annotation.RequiresApi;
-import androidx.camera.viewfinder.ViewfinderSurfaceRequest;
-
-/**
- * A interface implemented by the application to provide a {@link Surface} for viewfinder.
- *
- * <p> This interface is implemented by the application to provide a {@link Surface}. This
- * will be called by application when it needs a Surface for viewfinder. It also signals when the
- * Surface is no longer in use.
- */
-@RequiresApi(21) // TODO(b/200306659): Remove and replace with annotation on package-info.java
-public interface ViewfinderSurfaceProvider {
-    /**
-     * Called when a new {@link Surface} has been requested by the camera.
-     *
-     * <p>This is called every time a new surface is required to keep the viewfinder running.
-     * The camera may repeatedly request surfaces, but only a single request will be active at a
-     * time.
-     *
-     * @param request the request for a surface which contains the requirements of the
-     *                surface and methods for completing the request.
-     */
-    void onSurfaceRequested(@NonNull ViewfinderSurfaceRequest request);
-}
diff --git a/camera/camera-viewfinder/src/main/java/androidx/camera/viewfinder/internal/surface/package-info.java b/camera/camera-viewfinder/src/main/java/androidx/camera/viewfinder/internal/surface/package-info.java
deleted file mode 100644
index 30637d8..0000000
--- a/camera/camera-viewfinder/src/main/java/androidx/camera/viewfinder/internal/surface/package-info.java
+++ /dev/null
@@ -1,22 +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.
- */
-
-/**
- */
-@RestrictTo(RestrictTo.Scope.LIBRARY)
-package androidx.camera.viewfinder.internal.surface;
-
-import androidx.annotation.RestrictTo;
diff --git a/camera/camera-viewfinder/src/test/java/androidx/camera/viewfinder/CameraViewfinderTest.java b/camera/camera-viewfinder/src/test/java/androidx/camera/viewfinder/CameraViewfinderTest.java
index 2528c8d..bdf5010 100644
--- a/camera/camera-viewfinder/src/test/java/androidx/camera/viewfinder/CameraViewfinderTest.java
+++ b/camera/camera-viewfinder/src/test/java/androidx/camera/viewfinder/CameraViewfinderTest.java
@@ -16,6 +16,10 @@
 
 package androidx.camera.viewfinder;
 
+import static androidx.camera.viewfinder.CameraViewfinder.shouldUseTextureView;
+import static androidx.camera.viewfinder.surface.ImplementationMode.COMPATIBLE;
+import static androidx.camera.viewfinder.surface.ImplementationMode.PERFORMANCE;
+
 import static com.google.common.truth.Truth.assertThat;
 
 import android.os.Build;
@@ -45,8 +49,7 @@
     @Config(minSdk = Build.VERSION_CODES.N_MR1)
     public void surfaceViewNormal_useSurfaceView() {
         // Assert: SurfaceView is used.
-        assertThat(CameraViewfinder.shouldUseTextureView(
-                CameraViewfinder.ImplementationMode.PERFORMANCE)).isFalse();
+        assertThat(shouldUseTextureView(PERFORMANCE)).isFalse();
     }
 
     @Test
@@ -55,8 +58,7 @@
         QuirkInjector.inject(new SurfaceViewStretchedQuirk());
 
         // Assert: TextureView is used even the SurfaceRequest is compatible with SurfaceView.
-        assertThat(CameraViewfinder.shouldUseTextureView(
-                CameraViewfinder.ImplementationMode.PERFORMANCE)).isTrue();
+        assertThat(shouldUseTextureView(PERFORMANCE)).isTrue();
     }
 
     @Test
@@ -65,8 +67,7 @@
         QuirkInjector.inject(new SurfaceViewNotCroppedByParentQuirk());
 
         // Assert: TextureView is used even the SurfaceRequest is compatible with SurfaceView.
-        assertThat(CameraViewfinder.shouldUseTextureView(
-                CameraViewfinder.ImplementationMode.PERFORMANCE)).isTrue();
+        assertThat(shouldUseTextureView(PERFORMANCE)).isTrue();
     }
 
     @Test
@@ -75,7 +76,6 @@
         QuirkInjector.inject(new SurfaceViewNotCroppedByParentQuirk());
 
         // Assert: TextureView is used even the SurfaceRequest is compatible with SurfaceView.
-        assertThat(CameraViewfinder.shouldUseTextureView(
-                CameraViewfinder.ImplementationMode.COMPATIBLE)).isTrue();
+        assertThat(shouldUseTextureView(COMPATIBLE)).isTrue();
     }
 }
diff --git a/camera/integration-tests/coretestapp/src/main/cpp/CMakeLists.txt b/camera/integration-tests/coretestapp/src/main/cpp/CMakeLists.txt
index 49cca5a..1f9ba23 100644
--- a/camera/integration-tests/coretestapp/src/main/cpp/CMakeLists.txt
+++ b/camera/integration-tests/coretestapp/src/main/cpp/CMakeLists.txt
@@ -31,4 +31,9 @@
 find_library(egl-lib EGL)
 
 
-target_link_libraries(opengl_renderer_jni ${log-lib} ${android-lib} ${opengl-lib} ${egl-lib})
\ No newline at end of file
+target_link_libraries(opengl_renderer_jni ${log-lib} ${android-lib} ${opengl-lib} ${egl-lib})
+target_link_options(
+  opengl_renderer_jni
+  PRIVATE
+  "-Wl,-z,max-page-size=16384"
+)
diff --git a/camera/integration-tests/extensionstestapp/src/androidTest/java/androidx/camera/integration/extensions/ImageCaptureTest.kt b/camera/integration-tests/extensionstestapp/src/androidTest/java/androidx/camera/integration/extensions/ImageCaptureTest.kt
index e37be17..c32f705 100644
--- a/camera/integration-tests/extensionstestapp/src/androidTest/java/androidx/camera/integration/extensions/ImageCaptureTest.kt
+++ b/camera/integration-tests/extensionstestapp/src/androidTest/java/androidx/camera/integration/extensions/ImageCaptureTest.kt
@@ -31,7 +31,7 @@
 import androidx.camera.integration.extensions.util.CameraXExtensionsTestUtil.launchCameraExtensionsActivity
 import androidx.camera.integration.extensions.util.HOME_TIMEOUT_MS
 import androidx.camera.integration.extensions.util.takePictureAndWaitForImageSavedIdle
-import androidx.camera.integration.extensions.util.waitForPreviewViewIdle
+import androidx.camera.integration.extensions.util.waitForPreviewViewStreaming
 import androidx.camera.integration.extensions.utils.CameraIdExtensionModePair
 import androidx.camera.integration.extensions.utils.CameraSelectorUtil
 import androidx.camera.lifecycle.ProcessCameraProvider
@@ -160,7 +160,7 @@
 
         with(activityScenario) {
             use {
-                waitForPreviewViewIdle()
+                waitForPreviewViewStreaming()
                 val camera = withActivity { mCamera }
                 // Retrieves the session processor from the camera's extended config
                 val sessionProcessor = camera.extendedConfig.sessionProcessor
diff --git a/camera/integration-tests/viewfindertestapp/src/main/java/androidx/camera/integration/viewfinder/CameraViewfinderFoldableFragment.kt b/camera/integration-tests/viewfindertestapp/src/main/java/androidx/camera/integration/viewfinder/CameraViewfinderFoldableFragment.kt
index 3627dd5..9b944f1 100644
--- a/camera/integration-tests/viewfindertestapp/src/main/java/androidx/camera/integration/viewfinder/CameraViewfinderFoldableFragment.kt
+++ b/camera/integration-tests/viewfindertestapp/src/main/java/androidx/camera/integration/viewfinder/CameraViewfinderFoldableFragment.kt
@@ -56,11 +56,12 @@
 import androidx.appcompat.app.AlertDialog
 import androidx.camera.core.impl.utils.CompareSizesByArea
 import androidx.camera.viewfinder.CameraViewfinder
-import androidx.camera.viewfinder.CameraViewfinder.ImplementationMode
 import androidx.camera.viewfinder.CameraViewfinder.ScaleType
 import androidx.camera.viewfinder.CameraViewfinderExt.requestSurface
-import androidx.camera.viewfinder.ViewfinderSurfaceRequest
 import androidx.camera.viewfinder.populateFromCharacteristics
+import androidx.camera.viewfinder.surface.ImplementationMode
+import androidx.camera.viewfinder.surface.ViewfinderSurfaceRequest
+import androidx.camera.viewfinder.surface.populateFromCharacteristics
 import androidx.fragment.app.DialogFragment
 import androidx.fragment.app.Fragment
 import androidx.lifecycle.Observer
@@ -171,7 +172,7 @@
         when (item.itemId) {
             R.id.implementationMode -> {
                 val implementationMode =
-                    when (cameraViewfinder.implementationMode) {
+                    when (cameraViewfinder.surfaceImplementationMode) {
                         ImplementationMode.PERFORMANCE ->
                             ImplementationMode.COMPATIBLE
                         else -> ImplementationMode.PERFORMANCE
diff --git a/compose/animation/animation-core/src/androidUnitTest/kotlin/androidx/compose/animation/core/KeyframeAnimationTest.kt b/compose/animation/animation-core/src/androidUnitTest/kotlin/androidx/compose/animation/core/KeyframeAnimationTest.kt
index e497bbe..74b0866 100644
--- a/compose/animation/animation-core/src/androidUnitTest/kotlin/androidx/compose/animation/core/KeyframeAnimationTest.kt
+++ b/compose/animation/animation-core/src/androidUnitTest/kotlin/androidx/compose/animation/core/KeyframeAnimationTest.kt
@@ -233,4 +233,72 @@
 
         assertThat(animation.at(25)).isEqualTo(0.25f)
     }
+
+    @Test
+    fun outOfRangeValuesOnly() {
+        val duration = 100
+        val delay = 200
+
+        // Out of range values should be effectively ignored.
+        // It should interpolate within the expected time range without issues
+        val animation = keyframes<Float> {
+            durationMillis = duration
+            delayMillis = delay
+
+            -1f at -delay using LinearEasing
+            -2f at -duration using LinearEasing
+            -3f at (duration + 50) using LinearEasing
+        }.vectorize(Float.VectorConverter)
+
+        // Within delay, should always return initial value unless it was overwritten
+        assertThat(animation.at(0)).isEqualTo(0f)
+        assertThat(animation.at(100)).isEqualTo(0f)
+        assertThat(animation.at(200)).isEqualTo(0f)
+
+        // Within time range
+        assertThat(animation.at(delay)).isEqualTo(0f)
+        assertThat(animation.at((duration / 2) + delay)).isEqualTo(0.5f)
+        assertThat(animation.at(duration + delay)).isEqualTo(1f)
+
+        // Out of range - past animation duration
+        // Should always be the target value unless it was overwritten
+        assertThat(animation.at(delay + duration + 1)).isEqualTo(1f)
+        assertThat(animation.at(delay + duration + 50)).isEqualTo(1f)
+    }
+
+    @Test
+    fun outOfRangeValues_withForcedInitialAndTarget() {
+        val duration = 100
+        val delay = 200
+
+        // Out of range values should be effectively ignored.
+        // It should interpolate within the expected time range without issues
+        val animation = keyframes<Float> {
+            durationMillis = duration
+            delayMillis = delay
+
+            -1f at -delay using LinearEasing
+            -2f at -duration using LinearEasing
+            -3f at (duration + 50) using LinearEasing
+
+            // Force initial and target
+            4f at 0 using LinearEasing
+            5f at duration using LinearEasing
+        }.vectorize(Float.VectorConverter)
+
+        // Within delay, should always return initial value unless it was overwritten
+        assertThat(animation.at(0)).isEqualTo(4f)
+        assertThat(animation.at(100)).isEqualTo(4f)
+        assertThat(animation.at(200)).isEqualTo(4f)
+
+        // Within time range
+        assertThat(animation.at(delay)).isEqualTo(4f)
+        assertThat(animation.at((duration / 2) + delay)).isEqualTo(4.5f)
+        assertThat(animation.at(duration + delay)).isEqualTo(5f)
+
+        // Out of range - past animation duration
+        // Should always be the target value unless it was overwritten
+        assertThat(animation.at(delay + duration + 1)).isEqualTo(5f)
+        assertThat(animation.at(delay + duration + 50)).isEqualTo(5f)
+    }
 }
diff --git a/compose/animation/animation-core/src/commonMain/kotlin/androidx/compose/animation/core/Preconditions.kt b/compose/animation/animation-core/src/commonMain/kotlin/androidx/compose/animation/core/Preconditions.kt
index 2b38bbc..a6e01cd 100644
--- a/compose/animation/animation-core/src/commonMain/kotlin/androidx/compose/animation/core/Preconditions.kt
+++ b/compose/animation/animation-core/src/commonMain/kotlin/androidx/compose/animation/core/Preconditions.kt
@@ -22,7 +22,7 @@
 // This function exists so we do *not* inline the throw. It keeps
 // the call site much smaller and since it's the slow path anyway,
 // we don't mind the extra function call
-internal fun throwIllegalArgumentException(message: String): Nothing {
+internal fun throwIllegalArgumentException(message: String) {
     throw IllegalArgumentException(message)
 }
 
@@ -39,7 +39,11 @@
 }
 
 // See above
-internal fun throwIllegalStateException(message: String): Nothing {
+internal fun throwIllegalStateException(message: String) {
+    throw IllegalStateException(message)
+}
+
+internal fun throwIllegalStateExceptionForNullCheck(message: String): Nothing {
     throw IllegalStateException(message)
 }
 
@@ -64,7 +68,7 @@
     }
 
     if (value == null) {
-        throwIllegalStateException(lazyMessage())
+        throwIllegalStateExceptionForNullCheck(lazyMessage())
     }
     return value
 }
diff --git a/compose/animation/animation-core/src/commonMain/kotlin/androidx/compose/animation/core/VectorizedAnimationSpec.kt b/compose/animation/animation-core/src/commonMain/kotlin/androidx/compose/animation/core/VectorizedAnimationSpec.kt
index ddcaa2c..9d09a90 100644
--- a/compose/animation/animation-core/src/commonMain/kotlin/androidx/compose/animation/core/VectorizedAnimationSpec.kt
+++ b/compose/animation/animation-core/src/commonMain/kotlin/androidx/compose/animation/core/VectorizedAnimationSpec.kt
@@ -417,21 +417,21 @@
         val timestampStart = timestamps[index]
         val startValue: V = if (keyframes.contains(timestampStart)) {
             keyframes[timestampStart]!!.vectorValue
-        } else if (index == 0) {
-            // Use initial value if it wasn't overwritten by the user
-            initialValue
         } else {
-            throw IllegalStateException("No value to animate from at $clampedPlayTime millis")
+            // Use initial value if it wasn't overwritten by the user
+            // This is always the correct fallback assuming timestamps and keyframes were populated
+            // as expected
+            initialValue
         }
 
         val timestampEnd = timestamps[index + 1]
         val endValue = if (keyframes.contains(timestampEnd)) {
             keyframes[timestampEnd]!!.vectorValue
-        } else if (index + 1 == timestamps.size - 1) {
-            // Use target value if it wasn't overwritten by the user
-            targetValue
         } else {
-            throw IllegalStateException("No value to animate to at $clampedPlayTime millis")
+            // Use target value if it wasn't overwritten by the user
+            // This is always the correct fallback assuming timestamps and keyframes were populated
+            // as expected
+            targetValue
         }
 
         for (i in 0 until valueVector.size) {
diff --git a/compose/animation/animation/src/commonMain/kotlin/androidx/compose/animation/AnimationModifier.kt b/compose/animation/animation/src/commonMain/kotlin/androidx/compose/animation/AnimationModifier.kt
index a43b12d..3324d7d 100644
--- a/compose/animation/animation/src/commonMain/kotlin/androidx/compose/animation/AnimationModifier.kt
+++ b/compose/animation/animation/src/commonMain/kotlin/androidx/compose/animation/AnimationModifier.kt
@@ -126,6 +126,12 @@
 
     var animData: AnimData? by mutableStateOf(null)
 
+    override fun onReset() {
+        super.onReset()
+        // Reset is an indication that the node may be re-used, in such case, animData becomes stale
+        animData = null
+    }
+
     override fun onAttach() {
         super.onAttach()
         // When re-attached, we may be attached to a tree without lookahead scope.
diff --git a/compose/compiler/compiler-hosted/build.gradle b/compose/compiler/compiler-hosted/build.gradle
index dfa7c90..e51c401 100644
--- a/compose/compiler/compiler-hosted/build.gradle
+++ b/compose/compiler/compiler-hosted/build.gradle
@@ -47,6 +47,15 @@
     }
 }
 
+afterEvaluate {
+    lint {
+        lintOptions {
+            // Until fully switch to K2, existing FE1.0 usages are legitimate
+            disable("KotlincFE10")
+        }
+    }
+}
+
 androidx {
     name = "Compose Hosted Compiler Plugin"
     // This is only published because that is required when exporting it to g3.
diff --git a/compose/compiler/compiler-hosted/src/main/java/androidx/compose/compiler/plugins/kotlin/VersionChecker.kt b/compose/compiler/compiler-hosted/src/main/java/androidx/compose/compiler/plugins/kotlin/VersionChecker.kt
index db4656d..fcdbff4 100644
--- a/compose/compiler/compiler-hosted/src/main/java/androidx/compose/compiler/plugins/kotlin/VersionChecker.kt
+++ b/compose/compiler/compiler-hosted/src/main/java/androidx/compose/compiler/plugins/kotlin/VersionChecker.kt
@@ -130,6 +130,7 @@
             11700 to "1.6.0-alpha08",
             11800 to "1.6.0-beta01",
             12000 to "1.7.0-alpha01",
+            12100 to "1.7.0-alpha02",
         )
 
         /**
diff --git a/compose/foundation/foundation/api/current.ignore b/compose/foundation/foundation/api/current.ignore
index 022ce06..fba6623 100644
--- a/compose/foundation/foundation/api/current.ignore
+++ b/compose/foundation/foundation/api/current.ignore
@@ -1,6 +1,10 @@
 // Baseline format: 1.0
 AddedAbstractMethod: androidx.compose.foundation.pager.PageInfo#getKey():
     Added method androidx.compose.foundation.pager.PageInfo.getKey()
+AddedAbstractMethod: androidx.compose.foundation.pager.PagerLayoutInfo#getOutOfBoundsPageCount():
+    Added method androidx.compose.foundation.pager.PagerLayoutInfo.getOutOfBoundsPageCount()
+AddedAbstractMethod: androidx.compose.foundation.pager.PagerLayoutInfo#getSnapPosition():
+    Added method androidx.compose.foundation.pager.PagerLayoutInfo.getSnapPosition()
 
 
 ChangedType: androidx.compose.foundation.gestures.snapping.SnapFlingBehaviorKt#rememberSnapFlingBehavior(androidx.compose.foundation.gestures.snapping.SnapLayoutInfoProvider):
diff --git a/compose/foundation/foundation/api/current.txt b/compose/foundation/foundation/api/current.txt
index 512013a..3328399 100644
--- a/compose/foundation/foundation/api/current.txt
+++ b/compose/foundation/foundation/api/current.txt
@@ -501,7 +501,7 @@
 
   public final class DraggableKt {
     method public static androidx.compose.foundation.gestures.DraggableState DraggableState(kotlin.jvm.functions.Function1<? super java.lang.Float,kotlin.Unit> onDelta);
-    method public static androidx.compose.ui.Modifier draggable(androidx.compose.ui.Modifier, androidx.compose.foundation.gestures.DraggableState state, androidx.compose.foundation.gestures.Orientation orientation, optional boolean enabled, optional androidx.compose.foundation.interaction.MutableInteractionSource? interactionSource, optional boolean startDragImmediately, optional kotlin.jvm.functions.Function3<? super kotlinx.coroutines.CoroutineScope,? super androidx.compose.ui.geometry.Offset,? super kotlin.coroutines.Continuation<? super kotlin.Unit>,?> onDragStarted, optional kotlin.jvm.functions.Function3<? super kotlinx.coroutines.CoroutineScope,? super java.lang.Float,? super kotlin.coroutines.Continuation<? super kotlin.Unit>,?> onDragStopped, optional boolean reverseDirection);
+    method @androidx.compose.runtime.Stable public static androidx.compose.ui.Modifier draggable(androidx.compose.ui.Modifier, androidx.compose.foundation.gestures.DraggableState state, androidx.compose.foundation.gestures.Orientation orientation, optional boolean enabled, optional androidx.compose.foundation.interaction.MutableInteractionSource? interactionSource, optional boolean startDragImmediately, optional kotlin.jvm.functions.Function3<? super kotlinx.coroutines.CoroutineScope,? super androidx.compose.ui.geometry.Offset,? super kotlin.coroutines.Continuation<? super kotlin.Unit>,?> onDragStarted, optional kotlin.jvm.functions.Function3<? super kotlinx.coroutines.CoroutineScope,? super java.lang.Float,? super kotlin.coroutines.Continuation<? super kotlin.Unit>,?> onDragStopped, optional boolean reverseDirection);
     method @androidx.compose.runtime.Composable public static androidx.compose.foundation.gestures.DraggableState rememberDraggableState(kotlin.jvm.functions.Function1<? super java.lang.Float,kotlin.Unit> onDelta);
   }
 
@@ -834,6 +834,17 @@
     property public abstract java.util.List<androidx.compose.foundation.lazy.LazyListItemInfo> visibleItemsInfo;
   }
 
+  @SuppressCompatibility @androidx.compose.foundation.ExperimentalFoundationApi public interface LazyListPrefetchScope {
+    method public androidx.compose.foundation.lazy.layout.LazyLayoutPrefetchState.PrefetchHandle schedulePrefetch(int index);
+  }
+
+  @SuppressCompatibility @androidx.compose.foundation.ExperimentalFoundationApi public interface LazyListPrefetchStrategy {
+    method public default androidx.compose.foundation.lazy.layout.PrefetchExecutor? getPrefetchExecutor();
+    method public void onScroll(androidx.compose.foundation.lazy.LazyListPrefetchScope, float delta, androidx.compose.foundation.lazy.LazyListLayoutInfo layoutInfo);
+    method public void onVisibleItemsUpdated(androidx.compose.foundation.lazy.LazyListPrefetchScope, androidx.compose.foundation.lazy.LazyListLayoutInfo layoutInfo);
+    property public default androidx.compose.foundation.lazy.layout.PrefetchExecutor? prefetchExecutor;
+  }
+
   @androidx.compose.foundation.lazy.LazyScopeMarker @kotlin.jvm.JvmDefaultWithCompatibility public interface LazyListScope {
     method public default void item(optional Object? key, optional Object? contentType, kotlin.jvm.functions.Function1<? super androidx.compose.foundation.lazy.LazyItemScope,kotlin.Unit> content);
     method @Deprecated public void item(optional Object? key, kotlin.jvm.functions.Function1<? super androidx.compose.foundation.lazy.LazyItemScope,kotlin.Unit> content);
@@ -844,6 +855,7 @@
 
   @androidx.compose.runtime.Stable public final class LazyListState implements androidx.compose.foundation.gestures.ScrollableState {
     ctor public LazyListState(optional int firstVisibleItemIndex, optional int firstVisibleItemScrollOffset);
+    ctor @SuppressCompatibility @androidx.compose.foundation.ExperimentalFoundationApi public LazyListState(optional int firstVisibleItemIndex, optional int firstVisibleItemScrollOffset, optional androidx.compose.foundation.lazy.LazyListPrefetchStrategy prefetchStrategy);
     method public suspend Object? animateScrollToItem(@IntRange(from=0L) int index, optional int scrollOffset, kotlin.coroutines.Continuation<? super kotlin.Unit>);
     method public float dispatchRawDelta(float delta);
     method public int getFirstVisibleItemIndex();
@@ -851,6 +863,7 @@
     method public androidx.compose.foundation.interaction.InteractionSource getInteractionSource();
     method public androidx.compose.foundation.lazy.LazyListLayoutInfo getLayoutInfo();
     method public boolean isScrollInProgress();
+    method public void notifyPrefetchOnScroll(float delta, androidx.compose.foundation.lazy.LazyListLayoutInfo layoutInfo);
     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 suspend Object? scrollToItem(@IntRange(from=0L) int index, optional int scrollOffset, kotlin.coroutines.Continuation<? super kotlin.Unit>);
     property public boolean canScrollBackward;
@@ -870,6 +883,7 @@
 
   public final class LazyListStateKt {
     method @androidx.compose.runtime.Composable public static androidx.compose.foundation.lazy.LazyListState rememberLazyListState(optional int initialFirstVisibleItemIndex, optional int initialFirstVisibleItemScrollOffset);
+    method @SuppressCompatibility @androidx.compose.foundation.ExperimentalFoundationApi @androidx.compose.runtime.Composable public static androidx.compose.foundation.lazy.LazyListState rememberLazyListState(optional int initialFirstVisibleItemIndex, optional int initialFirstVisibleItemScrollOffset, optional androidx.compose.foundation.lazy.LazyListPrefetchStrategy prefetchStrategy);
   }
 
   @kotlin.DslMarker public @interface LazyScopeMarker {
@@ -1093,7 +1107,7 @@
   }
 
   @SuppressCompatibility @androidx.compose.foundation.ExperimentalFoundationApi @androidx.compose.runtime.Stable public final class LazyLayoutPrefetchState {
-    ctor public LazyLayoutPrefetchState();
+    ctor public LazyLayoutPrefetchState(optional androidx.compose.foundation.lazy.layout.PrefetchExecutor? prefetchExecutor);
     method public androidx.compose.foundation.lazy.layout.LazyLayoutPrefetchState.PrefetchHandle schedulePrefetch(int index, long constraints);
   }
 
@@ -1114,6 +1128,19 @@
     property public int size;
   }
 
+  @SuppressCompatibility @androidx.compose.foundation.ExperimentalFoundationApi public interface PrefetchExecutor {
+    method public void requestPrefetch(androidx.compose.foundation.lazy.layout.PrefetchExecutor.Request request);
+  }
+
+  public static sealed interface PrefetchExecutor.Request {
+    method public boolean isComposed();
+    method public boolean isValid();
+    method public void performComposition();
+    method public void performMeasure();
+    property public abstract boolean isComposed;
+    property public abstract boolean isValid;
+  }
+
 }
 
 package androidx.compose.foundation.lazy.staggeredgrid {
@@ -1261,8 +1288,7 @@
     property public final float pageSize;
   }
 
-  @SuppressCompatibility @androidx.compose.foundation.ExperimentalFoundationApi public final class PagerDefaults {
-    method @Deprecated @androidx.compose.runtime.Composable public androidx.compose.foundation.gestures.TargetedFlingBehavior flingBehavior(androidx.compose.foundation.pager.PagerState state, optional androidx.compose.foundation.pager.PagerSnapDistance pagerSnapDistance, optional androidx.compose.animation.core.AnimationSpec<java.lang.Float> lowVelocityAnimationSpec, optional androidx.compose.animation.core.DecayAnimationSpec<java.lang.Float> highVelocityAnimationSpec, optional androidx.compose.animation.core.AnimationSpec<java.lang.Float> snapAnimationSpec, optional float snapPositionalThreshold);
+  public final class PagerDefaults {
     method @androidx.compose.runtime.Composable public androidx.compose.foundation.gestures.TargetedFlingBehavior flingBehavior(androidx.compose.foundation.pager.PagerState state, optional androidx.compose.foundation.pager.PagerSnapDistance pagerSnapDistance, optional androidx.compose.animation.core.DecayAnimationSpec<java.lang.Float> decayAnimationSpec, optional androidx.compose.animation.core.AnimationSpec<java.lang.Float> snapAnimationSpec, optional @FloatRange(from=0.0, to=1.0) float snapPositionalThreshold);
     method public androidx.compose.ui.input.nestedscroll.NestedScrollConnection pageNestedScrollConnection(androidx.compose.foundation.pager.PagerState state, androidx.compose.foundation.gestures.Orientation orientation);
     field public static final androidx.compose.foundation.pager.PagerDefaults INSTANCE;
@@ -1270,11 +1296,11 @@
   }
 
   public final class PagerKt {
-    method @SuppressCompatibility @androidx.compose.foundation.ExperimentalFoundationApi @androidx.compose.runtime.Composable public static void HorizontalPager(androidx.compose.foundation.pager.PagerState state, optional androidx.compose.ui.Modifier modifier, optional androidx.compose.foundation.layout.PaddingValues contentPadding, optional androidx.compose.foundation.pager.PageSize pageSize, optional int outOfBoundsPageCount, optional float pageSpacing, optional androidx.compose.ui.Alignment.Vertical verticalAlignment, optional androidx.compose.foundation.gestures.TargetedFlingBehavior flingBehavior, optional boolean userScrollEnabled, optional boolean reverseLayout, optional kotlin.jvm.functions.Function1<? super java.lang.Integer,?>? key, optional androidx.compose.ui.input.nestedscroll.NestedScrollConnection pageNestedScrollConnection, optional androidx.compose.foundation.gestures.snapping.SnapPosition snapPosition, kotlin.jvm.functions.Function2<? super androidx.compose.foundation.pager.PagerScope,? super java.lang.Integer,kotlin.Unit> pageContent);
-    method @SuppressCompatibility @androidx.compose.foundation.ExperimentalFoundationApi @androidx.compose.runtime.Composable public static void VerticalPager(androidx.compose.foundation.pager.PagerState state, optional androidx.compose.ui.Modifier modifier, optional androidx.compose.foundation.layout.PaddingValues contentPadding, optional androidx.compose.foundation.pager.PageSize pageSize, optional int outOfBoundsPageCount, optional float pageSpacing, optional androidx.compose.ui.Alignment.Horizontal horizontalAlignment, optional androidx.compose.foundation.gestures.TargetedFlingBehavior flingBehavior, optional boolean userScrollEnabled, optional boolean reverseLayout, optional kotlin.jvm.functions.Function1<? super java.lang.Integer,?>? key, optional androidx.compose.ui.input.nestedscroll.NestedScrollConnection pageNestedScrollConnection, optional androidx.compose.foundation.gestures.snapping.SnapPosition snapPosition, kotlin.jvm.functions.Function2<? super androidx.compose.foundation.pager.PagerScope,? super java.lang.Integer,kotlin.Unit> pageContent);
+    method @androidx.compose.runtime.Composable public static void HorizontalPager(androidx.compose.foundation.pager.PagerState state, optional androidx.compose.ui.Modifier modifier, optional androidx.compose.foundation.layout.PaddingValues contentPadding, optional androidx.compose.foundation.pager.PageSize pageSize, optional int outOfBoundsPageCount, optional float pageSpacing, optional androidx.compose.ui.Alignment.Vertical verticalAlignment, optional androidx.compose.foundation.gestures.TargetedFlingBehavior flingBehavior, optional boolean userScrollEnabled, optional boolean reverseLayout, optional kotlin.jvm.functions.Function1<? super java.lang.Integer,?>? key, optional androidx.compose.ui.input.nestedscroll.NestedScrollConnection pageNestedScrollConnection, optional androidx.compose.foundation.gestures.snapping.SnapPosition snapPosition, kotlin.jvm.functions.Function2<? super androidx.compose.foundation.pager.PagerScope,? super java.lang.Integer,kotlin.Unit> pageContent);
+    method @androidx.compose.runtime.Composable public static void VerticalPager(androidx.compose.foundation.pager.PagerState state, optional androidx.compose.ui.Modifier modifier, optional androidx.compose.foundation.layout.PaddingValues contentPadding, optional androidx.compose.foundation.pager.PageSize pageSize, optional int outOfBoundsPageCount, optional float pageSpacing, optional androidx.compose.ui.Alignment.Horizontal horizontalAlignment, optional androidx.compose.foundation.gestures.TargetedFlingBehavior flingBehavior, optional boolean userScrollEnabled, optional boolean reverseLayout, optional kotlin.jvm.functions.Function1<? super java.lang.Integer,?>? key, optional androidx.compose.ui.input.nestedscroll.NestedScrollConnection pageNestedScrollConnection, optional androidx.compose.foundation.gestures.snapping.SnapPosition snapPosition, kotlin.jvm.functions.Function2<? super androidx.compose.foundation.pager.PagerScope,? super java.lang.Integer,kotlin.Unit> pageContent);
   }
 
-  @SuppressCompatibility @androidx.compose.foundation.ExperimentalFoundationApi public sealed interface PagerLayoutInfo {
+  public sealed interface PagerLayoutInfo {
     method public int getAfterContentPadding();
     method public int getBeforeContentPadding();
     method public androidx.compose.foundation.gestures.Orientation getOrientation();
@@ -1313,7 +1339,7 @@
     method public androidx.compose.foundation.pager.PagerSnapDistance atMost(int pages);
   }
 
-  @SuppressCompatibility @androidx.compose.foundation.ExperimentalFoundationApi @androidx.compose.runtime.Stable public abstract class PagerState implements androidx.compose.foundation.gestures.ScrollableState {
+  @androidx.compose.runtime.Stable public abstract class PagerState implements androidx.compose.foundation.gestures.ScrollableState {
     ctor public PagerState(optional int currentPage, optional @FloatRange(from=-0.5, to=0.5) float currentPageOffsetFraction);
     method public final suspend Object? animateScrollToPage(int page, optional @FloatRange(from=-0.5, to=0.5) float pageOffsetFraction, optional androidx.compose.animation.core.AnimationSpec<java.lang.Float> animationSpec, kotlin.coroutines.Continuation<? super kotlin.Unit>);
     method public float dispatchRawDelta(float delta);
@@ -1330,8 +1356,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 @FloatRange(from=-0.5, to=0.5) float pageOffsetFraction, kotlin.coroutines.Continuation<? super kotlin.Unit>);
-    method public final void updateCurrentPage(androidx.compose.foundation.gestures.ScrollScope, int page, optional @FloatRange(from=-0.5, to=0.5) float pageOffsetFraction);
-    method public final void updateTargetPage(androidx.compose.foundation.gestures.ScrollScope, int targetPage);
+    method @SuppressCompatibility @androidx.compose.foundation.ExperimentalFoundationApi public final void updateCurrentPage(androidx.compose.foundation.gestures.ScrollScope, int page, optional @FloatRange(from=-0.5, to=0.5) float pageOffsetFraction);
+    method @SuppressCompatibility @androidx.compose.foundation.ExperimentalFoundationApi 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;
@@ -1345,8 +1371,8 @@
   }
 
   public final class PagerStateKt {
-    method @SuppressCompatibility @androidx.compose.foundation.ExperimentalFoundationApi public static androidx.compose.foundation.pager.PagerState PagerState(optional int currentPage, optional @FloatRange(from=-0.5, to=0.5) float currentPageOffsetFraction, kotlin.jvm.functions.Function0<java.lang.Integer> pageCount);
-    method @SuppressCompatibility @androidx.compose.foundation.ExperimentalFoundationApi @androidx.compose.runtime.Composable public static androidx.compose.foundation.pager.PagerState rememberPagerState(optional int initialPage, optional @FloatRange(from=-0.5, to=0.5) float initialPageOffsetFraction, kotlin.jvm.functions.Function0<java.lang.Integer> pageCount);
+    method public static androidx.compose.foundation.pager.PagerState PagerState(optional int currentPage, optional @FloatRange(from=-0.5, to=0.5) float currentPageOffsetFraction, kotlin.jvm.functions.Function0<java.lang.Integer> pageCount);
+    method @androidx.compose.runtime.Composable public static androidx.compose.foundation.pager.PagerState rememberPagerState(optional int initialPage, optional @FloatRange(from=-0.5, to=0.5) float initialPageOffsetFraction, kotlin.jvm.functions.Function0<java.lang.Integer> pageCount);
   }
 
 }
diff --git a/compose/foundation/foundation/api/restricted_current.ignore b/compose/foundation/foundation/api/restricted_current.ignore
index 022ce06..fba6623 100644
--- a/compose/foundation/foundation/api/restricted_current.ignore
+++ b/compose/foundation/foundation/api/restricted_current.ignore
@@ -1,6 +1,10 @@
 // Baseline format: 1.0
 AddedAbstractMethod: androidx.compose.foundation.pager.PageInfo#getKey():
     Added method androidx.compose.foundation.pager.PageInfo.getKey()
+AddedAbstractMethod: androidx.compose.foundation.pager.PagerLayoutInfo#getOutOfBoundsPageCount():
+    Added method androidx.compose.foundation.pager.PagerLayoutInfo.getOutOfBoundsPageCount()
+AddedAbstractMethod: androidx.compose.foundation.pager.PagerLayoutInfo#getSnapPosition():
+    Added method androidx.compose.foundation.pager.PagerLayoutInfo.getSnapPosition()
 
 
 ChangedType: androidx.compose.foundation.gestures.snapping.SnapFlingBehaviorKt#rememberSnapFlingBehavior(androidx.compose.foundation.gestures.snapping.SnapLayoutInfoProvider):
diff --git a/compose/foundation/foundation/api/restricted_current.txt b/compose/foundation/foundation/api/restricted_current.txt
index 99e40d4..e0a9478 100644
--- a/compose/foundation/foundation/api/restricted_current.txt
+++ b/compose/foundation/foundation/api/restricted_current.txt
@@ -503,7 +503,7 @@
 
   public final class DraggableKt {
     method public static androidx.compose.foundation.gestures.DraggableState DraggableState(kotlin.jvm.functions.Function1<? super java.lang.Float,kotlin.Unit> onDelta);
-    method public static androidx.compose.ui.Modifier draggable(androidx.compose.ui.Modifier, androidx.compose.foundation.gestures.DraggableState state, androidx.compose.foundation.gestures.Orientation orientation, optional boolean enabled, optional androidx.compose.foundation.interaction.MutableInteractionSource? interactionSource, optional boolean startDragImmediately, optional kotlin.jvm.functions.Function3<? super kotlinx.coroutines.CoroutineScope,? super androidx.compose.ui.geometry.Offset,? super kotlin.coroutines.Continuation<? super kotlin.Unit>,?> onDragStarted, optional kotlin.jvm.functions.Function3<? super kotlinx.coroutines.CoroutineScope,? super java.lang.Float,? super kotlin.coroutines.Continuation<? super kotlin.Unit>,?> onDragStopped, optional boolean reverseDirection);
+    method @androidx.compose.runtime.Stable public static androidx.compose.ui.Modifier draggable(androidx.compose.ui.Modifier, androidx.compose.foundation.gestures.DraggableState state, androidx.compose.foundation.gestures.Orientation orientation, optional boolean enabled, optional androidx.compose.foundation.interaction.MutableInteractionSource? interactionSource, optional boolean startDragImmediately, optional kotlin.jvm.functions.Function3<? super kotlinx.coroutines.CoroutineScope,? super androidx.compose.ui.geometry.Offset,? super kotlin.coroutines.Continuation<? super kotlin.Unit>,?> onDragStarted, optional kotlin.jvm.functions.Function3<? super kotlinx.coroutines.CoroutineScope,? super java.lang.Float,? super kotlin.coroutines.Continuation<? super kotlin.Unit>,?> onDragStopped, optional boolean reverseDirection);
     method @androidx.compose.runtime.Composable public static androidx.compose.foundation.gestures.DraggableState rememberDraggableState(kotlin.jvm.functions.Function1<? super java.lang.Float,kotlin.Unit> onDelta);
   }
 
@@ -836,6 +836,17 @@
     property public abstract java.util.List<androidx.compose.foundation.lazy.LazyListItemInfo> visibleItemsInfo;
   }
 
+  @SuppressCompatibility @androidx.compose.foundation.ExperimentalFoundationApi public interface LazyListPrefetchScope {
+    method public androidx.compose.foundation.lazy.layout.LazyLayoutPrefetchState.PrefetchHandle schedulePrefetch(int index);
+  }
+
+  @SuppressCompatibility @androidx.compose.foundation.ExperimentalFoundationApi public interface LazyListPrefetchStrategy {
+    method public default androidx.compose.foundation.lazy.layout.PrefetchExecutor? getPrefetchExecutor();
+    method public void onScroll(androidx.compose.foundation.lazy.LazyListPrefetchScope, float delta, androidx.compose.foundation.lazy.LazyListLayoutInfo layoutInfo);
+    method public void onVisibleItemsUpdated(androidx.compose.foundation.lazy.LazyListPrefetchScope, androidx.compose.foundation.lazy.LazyListLayoutInfo layoutInfo);
+    property public default androidx.compose.foundation.lazy.layout.PrefetchExecutor? prefetchExecutor;
+  }
+
   @androidx.compose.foundation.lazy.LazyScopeMarker @kotlin.jvm.JvmDefaultWithCompatibility public interface LazyListScope {
     method public default void item(optional Object? key, optional Object? contentType, kotlin.jvm.functions.Function1<? super androidx.compose.foundation.lazy.LazyItemScope,kotlin.Unit> content);
     method @Deprecated public void item(optional Object? key, kotlin.jvm.functions.Function1<? super androidx.compose.foundation.lazy.LazyItemScope,kotlin.Unit> content);
@@ -846,6 +857,7 @@
 
   @androidx.compose.runtime.Stable public final class LazyListState implements androidx.compose.foundation.gestures.ScrollableState {
     ctor public LazyListState(optional int firstVisibleItemIndex, optional int firstVisibleItemScrollOffset);
+    ctor @SuppressCompatibility @androidx.compose.foundation.ExperimentalFoundationApi public LazyListState(optional int firstVisibleItemIndex, optional int firstVisibleItemScrollOffset, optional androidx.compose.foundation.lazy.LazyListPrefetchStrategy prefetchStrategy);
     method public suspend Object? animateScrollToItem(@IntRange(from=0L) int index, optional int scrollOffset, kotlin.coroutines.Continuation<? super kotlin.Unit>);
     method public float dispatchRawDelta(float delta);
     method public int getFirstVisibleItemIndex();
@@ -853,6 +865,7 @@
     method public androidx.compose.foundation.interaction.InteractionSource getInteractionSource();
     method public androidx.compose.foundation.lazy.LazyListLayoutInfo getLayoutInfo();
     method public boolean isScrollInProgress();
+    method public void notifyPrefetchOnScroll(float delta, androidx.compose.foundation.lazy.LazyListLayoutInfo layoutInfo);
     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 suspend Object? scrollToItem(@IntRange(from=0L) int index, optional int scrollOffset, kotlin.coroutines.Continuation<? super kotlin.Unit>);
     property public boolean canScrollBackward;
@@ -872,6 +885,7 @@
 
   public final class LazyListStateKt {
     method @androidx.compose.runtime.Composable public static androidx.compose.foundation.lazy.LazyListState rememberLazyListState(optional int initialFirstVisibleItemIndex, optional int initialFirstVisibleItemScrollOffset);
+    method @SuppressCompatibility @androidx.compose.foundation.ExperimentalFoundationApi @androidx.compose.runtime.Composable public static androidx.compose.foundation.lazy.LazyListState rememberLazyListState(optional int initialFirstVisibleItemIndex, optional int initialFirstVisibleItemScrollOffset, optional androidx.compose.foundation.lazy.LazyListPrefetchStrategy prefetchStrategy);
   }
 
   @kotlin.DslMarker public @interface LazyScopeMarker {
@@ -1095,7 +1109,7 @@
   }
 
   @SuppressCompatibility @androidx.compose.foundation.ExperimentalFoundationApi @androidx.compose.runtime.Stable public final class LazyLayoutPrefetchState {
-    ctor public LazyLayoutPrefetchState();
+    ctor public LazyLayoutPrefetchState(optional androidx.compose.foundation.lazy.layout.PrefetchExecutor? prefetchExecutor);
     method public androidx.compose.foundation.lazy.layout.LazyLayoutPrefetchState.PrefetchHandle schedulePrefetch(int index, long constraints);
   }
 
@@ -1116,6 +1130,19 @@
     property public int size;
   }
 
+  @SuppressCompatibility @androidx.compose.foundation.ExperimentalFoundationApi public interface PrefetchExecutor {
+    method public void requestPrefetch(androidx.compose.foundation.lazy.layout.PrefetchExecutor.Request request);
+  }
+
+  public static sealed interface PrefetchExecutor.Request {
+    method public boolean isComposed();
+    method public boolean isValid();
+    method public void performComposition();
+    method public void performMeasure();
+    property public abstract boolean isComposed;
+    property public abstract boolean isValid;
+  }
+
 }
 
 package androidx.compose.foundation.lazy.staggeredgrid {
@@ -1263,8 +1290,7 @@
     property public final float pageSize;
   }
 
-  @SuppressCompatibility @androidx.compose.foundation.ExperimentalFoundationApi public final class PagerDefaults {
-    method @Deprecated @androidx.compose.runtime.Composable public androidx.compose.foundation.gestures.TargetedFlingBehavior flingBehavior(androidx.compose.foundation.pager.PagerState state, optional androidx.compose.foundation.pager.PagerSnapDistance pagerSnapDistance, optional androidx.compose.animation.core.AnimationSpec<java.lang.Float> lowVelocityAnimationSpec, optional androidx.compose.animation.core.DecayAnimationSpec<java.lang.Float> highVelocityAnimationSpec, optional androidx.compose.animation.core.AnimationSpec<java.lang.Float> snapAnimationSpec, optional float snapPositionalThreshold);
+  public final class PagerDefaults {
     method @androidx.compose.runtime.Composable public androidx.compose.foundation.gestures.TargetedFlingBehavior flingBehavior(androidx.compose.foundation.pager.PagerState state, optional androidx.compose.foundation.pager.PagerSnapDistance pagerSnapDistance, optional androidx.compose.animation.core.DecayAnimationSpec<java.lang.Float> decayAnimationSpec, optional androidx.compose.animation.core.AnimationSpec<java.lang.Float> snapAnimationSpec, optional @FloatRange(from=0.0, to=1.0) float snapPositionalThreshold);
     method public androidx.compose.ui.input.nestedscroll.NestedScrollConnection pageNestedScrollConnection(androidx.compose.foundation.pager.PagerState state, androidx.compose.foundation.gestures.Orientation orientation);
     field public static final androidx.compose.foundation.pager.PagerDefaults INSTANCE;
@@ -1272,11 +1298,11 @@
   }
 
   public final class PagerKt {
-    method @SuppressCompatibility @androidx.compose.foundation.ExperimentalFoundationApi @androidx.compose.runtime.Composable public static void HorizontalPager(androidx.compose.foundation.pager.PagerState state, optional androidx.compose.ui.Modifier modifier, optional androidx.compose.foundation.layout.PaddingValues contentPadding, optional androidx.compose.foundation.pager.PageSize pageSize, optional int outOfBoundsPageCount, optional float pageSpacing, optional androidx.compose.ui.Alignment.Vertical verticalAlignment, optional androidx.compose.foundation.gestures.TargetedFlingBehavior flingBehavior, optional boolean userScrollEnabled, optional boolean reverseLayout, optional kotlin.jvm.functions.Function1<? super java.lang.Integer,?>? key, optional androidx.compose.ui.input.nestedscroll.NestedScrollConnection pageNestedScrollConnection, optional androidx.compose.foundation.gestures.snapping.SnapPosition snapPosition, kotlin.jvm.functions.Function2<? super androidx.compose.foundation.pager.PagerScope,? super java.lang.Integer,kotlin.Unit> pageContent);
-    method @SuppressCompatibility @androidx.compose.foundation.ExperimentalFoundationApi @androidx.compose.runtime.Composable public static void VerticalPager(androidx.compose.foundation.pager.PagerState state, optional androidx.compose.ui.Modifier modifier, optional androidx.compose.foundation.layout.PaddingValues contentPadding, optional androidx.compose.foundation.pager.PageSize pageSize, optional int outOfBoundsPageCount, optional float pageSpacing, optional androidx.compose.ui.Alignment.Horizontal horizontalAlignment, optional androidx.compose.foundation.gestures.TargetedFlingBehavior flingBehavior, optional boolean userScrollEnabled, optional boolean reverseLayout, optional kotlin.jvm.functions.Function1<? super java.lang.Integer,?>? key, optional androidx.compose.ui.input.nestedscroll.NestedScrollConnection pageNestedScrollConnection, optional androidx.compose.foundation.gestures.snapping.SnapPosition snapPosition, kotlin.jvm.functions.Function2<? super androidx.compose.foundation.pager.PagerScope,? super java.lang.Integer,kotlin.Unit> pageContent);
+    method @androidx.compose.runtime.Composable public static void HorizontalPager(androidx.compose.foundation.pager.PagerState state, optional androidx.compose.ui.Modifier modifier, optional androidx.compose.foundation.layout.PaddingValues contentPadding, optional androidx.compose.foundation.pager.PageSize pageSize, optional int outOfBoundsPageCount, optional float pageSpacing, optional androidx.compose.ui.Alignment.Vertical verticalAlignment, optional androidx.compose.foundation.gestures.TargetedFlingBehavior flingBehavior, optional boolean userScrollEnabled, optional boolean reverseLayout, optional kotlin.jvm.functions.Function1<? super java.lang.Integer,?>? key, optional androidx.compose.ui.input.nestedscroll.NestedScrollConnection pageNestedScrollConnection, optional androidx.compose.foundation.gestures.snapping.SnapPosition snapPosition, kotlin.jvm.functions.Function2<? super androidx.compose.foundation.pager.PagerScope,? super java.lang.Integer,kotlin.Unit> pageContent);
+    method @androidx.compose.runtime.Composable public static void VerticalPager(androidx.compose.foundation.pager.PagerState state, optional androidx.compose.ui.Modifier modifier, optional androidx.compose.foundation.layout.PaddingValues contentPadding, optional androidx.compose.foundation.pager.PageSize pageSize, optional int outOfBoundsPageCount, optional float pageSpacing, optional androidx.compose.ui.Alignment.Horizontal horizontalAlignment, optional androidx.compose.foundation.gestures.TargetedFlingBehavior flingBehavior, optional boolean userScrollEnabled, optional boolean reverseLayout, optional kotlin.jvm.functions.Function1<? super java.lang.Integer,?>? key, optional androidx.compose.ui.input.nestedscroll.NestedScrollConnection pageNestedScrollConnection, optional androidx.compose.foundation.gestures.snapping.SnapPosition snapPosition, kotlin.jvm.functions.Function2<? super androidx.compose.foundation.pager.PagerScope,? super java.lang.Integer,kotlin.Unit> pageContent);
   }
 
-  @SuppressCompatibility @androidx.compose.foundation.ExperimentalFoundationApi public sealed interface PagerLayoutInfo {
+  public sealed interface PagerLayoutInfo {
     method public int getAfterContentPadding();
     method public int getBeforeContentPadding();
     method public androidx.compose.foundation.gestures.Orientation getOrientation();
@@ -1315,7 +1341,7 @@
     method public androidx.compose.foundation.pager.PagerSnapDistance atMost(int pages);
   }
 
-  @SuppressCompatibility @androidx.compose.foundation.ExperimentalFoundationApi @androidx.compose.runtime.Stable public abstract class PagerState implements androidx.compose.foundation.gestures.ScrollableState {
+  @androidx.compose.runtime.Stable public abstract class PagerState implements androidx.compose.foundation.gestures.ScrollableState {
     ctor public PagerState(optional int currentPage, optional @FloatRange(from=-0.5, to=0.5) float currentPageOffsetFraction);
     method public final suspend Object? animateScrollToPage(int page, optional @FloatRange(from=-0.5, to=0.5) float pageOffsetFraction, optional androidx.compose.animation.core.AnimationSpec<java.lang.Float> animationSpec, kotlin.coroutines.Continuation<? super kotlin.Unit>);
     method public float dispatchRawDelta(float delta);
@@ -1332,8 +1358,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 @FloatRange(from=-0.5, to=0.5) float pageOffsetFraction, kotlin.coroutines.Continuation<? super kotlin.Unit>);
-    method public final void updateCurrentPage(androidx.compose.foundation.gestures.ScrollScope, int page, optional @FloatRange(from=-0.5, to=0.5) float pageOffsetFraction);
-    method public final void updateTargetPage(androidx.compose.foundation.gestures.ScrollScope, int targetPage);
+    method @SuppressCompatibility @androidx.compose.foundation.ExperimentalFoundationApi public final void updateCurrentPage(androidx.compose.foundation.gestures.ScrollScope, int page, optional @FloatRange(from=-0.5, to=0.5) float pageOffsetFraction);
+    method @SuppressCompatibility @androidx.compose.foundation.ExperimentalFoundationApi 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;
@@ -1347,8 +1373,8 @@
   }
 
   public final class PagerStateKt {
-    method @SuppressCompatibility @androidx.compose.foundation.ExperimentalFoundationApi public static androidx.compose.foundation.pager.PagerState PagerState(optional int currentPage, optional @FloatRange(from=-0.5, to=0.5) float currentPageOffsetFraction, kotlin.jvm.functions.Function0<java.lang.Integer> pageCount);
-    method @SuppressCompatibility @androidx.compose.foundation.ExperimentalFoundationApi @androidx.compose.runtime.Composable public static androidx.compose.foundation.pager.PagerState rememberPagerState(optional int initialPage, optional @FloatRange(from=-0.5, to=0.5) float initialPageOffsetFraction, kotlin.jvm.functions.Function0<java.lang.Integer> pageCount);
+    method public static androidx.compose.foundation.pager.PagerState PagerState(optional int currentPage, optional @FloatRange(from=-0.5, to=0.5) float currentPageOffsetFraction, kotlin.jvm.functions.Function0<java.lang.Integer> pageCount);
+    method @androidx.compose.runtime.Composable public static androidx.compose.foundation.pager.PagerState rememberPagerState(optional int initialPage, optional @FloatRange(from=-0.5, to=0.5) float initialPageOffsetFraction, kotlin.jvm.functions.Function0<java.lang.Integer> pageCount);
   }
 
 }
diff --git a/compose/foundation/foundation/benchmark/src/androidTest/java/androidx/compose/foundation/benchmark/lazy/LazySwitchingStatesBenchmark.kt b/compose/foundation/foundation/benchmark/src/androidTest/java/androidx/compose/foundation/benchmark/lazy/LazySwitchingStatesBenchmark.kt
index 2cc62ed..8df6bf0 100644
--- a/compose/foundation/foundation/benchmark/src/androidTest/java/androidx/compose/foundation/benchmark/lazy/LazySwitchingStatesBenchmark.kt
+++ b/compose/foundation/foundation/benchmark/src/androidTest/java/androidx/compose/foundation/benchmark/lazy/LazySwitchingStatesBenchmark.kt
@@ -49,15 +49,28 @@
 
     @Test
     fun lazyColumn_switchingItems_composition() {
-        benchmarkRule.runBenchmark(composition = true)
+        benchmarkRule.runBenchmark(composition = true, switchingStateCount = NUMBER_OF_LAZY_ITEMS)
     }
 
     @Test
     fun lazyColumn_switchingItems_measure() {
-        benchmarkRule.runBenchmark(composition = false)
+        benchmarkRule.runBenchmark(composition = false, switchingStateCount = NUMBER_OF_LAZY_ITEMS)
     }
 
-    private fun ComposeBenchmarkRule.runBenchmark(composition: Boolean) {
+    @Test
+    fun lazyColumn_switchingItems_composition_one_state() {
+        benchmarkRule.runBenchmark(composition = true, switchingStateCount = 1)
+    }
+
+    @Test
+    fun lazyColumn_switchingItems_measure_one_state() {
+        benchmarkRule.runBenchmark(composition = false, switchingStateCount = 1)
+    }
+
+    private fun ComposeBenchmarkRule.runBenchmark(
+        composition: Boolean,
+        switchingStateCount: Int
+    ) {
         runBenchmarkFor(
             { LazyColumnSwitchingItemsCase(readInComposition = composition) },
         ) {
@@ -69,14 +82,14 @@
             measureRepeatedOnUiThread {
                 runWithTimingDisabled {
                     assertNoPendingChanges()
-                    repeat(getTestCase().items.size) {
+                    repeat(switchingStateCount) {
                         getTestCase().toggle(it)
                     }
                     doFramesUntilIdle()
                     assertNoPendingChanges()
                 }
 
-                repeat(getTestCase().items.size) {
+                repeat(switchingStateCount) {
                     getTestCase().toggle(it)
                 }
                 doFramesUntilIdle()
@@ -85,11 +98,12 @@
     }
 }
 
+// The number is based on height of items below (20 visible + 5 extra).
+private const val NUMBER_OF_LAZY_ITEMS = 25
 class LazyColumnSwitchingItemsCase(
     private val readInComposition: Boolean = false
 ) : ComposeTestCase {
-    // The number is based on height of items below (20 visible + 5 extra).
-    val items = List(25) {
+    val items = List(NUMBER_OF_LAZY_ITEMS) {
         mutableStateOf(false)
     }
 
diff --git a/compose/foundation/foundation/integration-tests/foundation-demos/build.gradle b/compose/foundation/foundation/integration-tests/foundation-demos/build.gradle
index ff44780..35eb15d 100644
--- a/compose/foundation/foundation/integration-tests/foundation-demos/build.gradle
+++ b/compose/foundation/foundation/integration-tests/foundation-demos/build.gradle
@@ -26,6 +26,7 @@
     implementation("androidx.core:core:1.12.0")
     implementation(libs.kotlinStdlib)
 
+    implementation(project(":activity:activity-compose"))
     implementation(project(":compose:animation:animation"))
     implementation(project(":compose:foundation:foundation"))
     implementation(project(":compose:foundation:foundation-layout"))
diff --git a/compose/foundation/foundation/integration-tests/foundation-demos/src/main/java/androidx/compose/foundation/demos/text/Hyperlinks.kt b/compose/foundation/foundation/integration-tests/foundation-demos/src/main/java/androidx/compose/foundation/demos/text/Hyperlinks.kt
index a8bfcbd..828d57d 100644
--- a/compose/foundation/foundation/integration-tests/foundation-demos/src/main/java/androidx/compose/foundation/demos/text/Hyperlinks.kt
+++ b/compose/foundation/foundation/integration-tests/foundation-demos/src/main/java/androidx/compose/foundation/demos/text/Hyperlinks.kt
@@ -159,7 +159,9 @@
             val inlineTextContent = InlineTextContent(
                 placeholder = Placeholder(fontSize, fontSize, PlaceholderVerticalAlign.Center)
             ) {
-                Box(modifier = Modifier.fillMaxSize().background(Color.Green))
+                Box(modifier = Modifier
+                    .fillMaxSize()
+                    .background(Color.Green))
             }
             Text(buildAnnotatedString {
                 append("A ")
@@ -216,6 +218,29 @@
                 }
             )
         }
+        Sample("RTL text") {
+            val text = buildAnnotatedString {
+                withAnnotation(LinkAnnotation.Url(LongWebLink)) {
+                    append(loremIpsum(Language.Arabic, 2))
+                }
+                append(loremIpsum(Language.Arabic, 5))
+                withAnnotation(LinkAnnotation.Url(LongWebLink)) {
+                    append(loremIpsum(Language.Arabic, 3))
+                }
+                append(loremIpsum(Language.Arabic, 5))
+            }
+            Text(text)
+        }
+        Sample("Bidi text") {
+            val text = buildAnnotatedString {
+                append(loremIpsum(Language.Arabic, 2))
+                withAnnotation(LinkAnnotation.Url(LongWebLink)) {
+                    append(" developer.android.com ")
+                }
+                append(loremIpsum(Language.Arabic, 5))
+            }
+            Text(text)
+        }
     }
 }
 
@@ -225,7 +250,8 @@
         Modifier
             .fillMaxWidth()
             .border(2.dp, Color.Black)
-            .padding(8.dp)) {
+            .padding(8.dp)
+    ) {
         Text(title, Modifier.align(Alignment.CenterHorizontally), fontWeight = FontWeight.Bold)
         content()
     }
diff --git a/compose/foundation/foundation/src/androidInstrumentedTest/kotlin/androidx/compose/foundation/DraggableTest.kt b/compose/foundation/foundation/src/androidInstrumentedTest/kotlin/androidx/compose/foundation/DraggableTest.kt
index 4efffb4..511924a 100644
--- a/compose/foundation/foundation/src/androidInstrumentedTest/kotlin/androidx/compose/foundation/DraggableTest.kt
+++ b/compose/foundation/foundation/src/androidInstrumentedTest/kotlin/androidx/compose/foundation/DraggableTest.kt
@@ -1007,7 +1007,6 @@
             assertThat(modifier.inspectableElements.map { it.name }.asIterable()).containsExactly(
                 "orientation",
                 "enabled",
-                "canDrag",
                 "reverseDirection",
                 "interactionSource",
                 "startDragImmediately",
@@ -1018,6 +1017,18 @@
         }
     }
 
+    @Test
+    fun equalInputs_shouldResolveToEquals() {
+        val state = DraggableState { }
+
+        val firstModifier = Modifier.draggable(state, Orientation.Horizontal)
+        val secondModifier = Modifier.draggable(state, Orientation.Vertical)
+        val thirdModifier = Modifier.draggable(state, Orientation.Horizontal)
+
+        assertThat(firstModifier).isEqualTo(thirdModifier)
+        assertThat(firstModifier).isNotEqualTo(secondModifier)
+    }
+
     private fun setDraggableContent(draggableFactory: @Composable () -> Modifier) {
         rule.setContent {
             Box {
diff --git a/compose/foundation/foundation/src/androidInstrumentedTest/kotlin/androidx/compose/foundation/content/TestDragAndDrop.kt b/compose/foundation/foundation/src/androidInstrumentedTest/kotlin/androidx/compose/foundation/content/TestDragAndDrop.kt
index bb04b09..e7721c3 100644
--- a/compose/foundation/foundation/src/androidInstrumentedTest/kotlin/androidx/compose/foundation/content/TestDragAndDrop.kt
+++ b/compose/foundation/foundation/src/androidInstrumentedTest/kotlin/androidx/compose/foundation/content/TestDragAndDrop.kt
@@ -17,7 +17,6 @@
 package androidx.compose.foundation.content
 
 import android.content.ClipData
-import android.content.ClipDescription
 import android.net.Uri
 import android.view.DragEvent
 import android.view.View
@@ -26,48 +25,85 @@
 import androidx.compose.ui.unit.Density
 
 /**
- * A helper scope creator to test multi-window Drag And Drop interactions.
+ * A helper to test multi-window Drag And Drop interactions.
  */
 internal fun testDragAndDrop(view: View, density: Density, block: DragAndDropScope.() -> Unit) {
     DragAndDropScopeImpl(view, density).block()
 }
 
+internal interface DragAndDropScope : Density {
+
+    /**
+     * Drags an item with ClipData that only holds the given [text] to the [offset] location.
+     */
+    fun drag(offset: Offset, text: String): Boolean
+
+    /**
+     * Drags an item with ClipData that only holds the given [uri] to the [offset] location.
+     */
+    fun drag(offset: Offset, uri: Uri): Boolean
+
+    /**
+     * Drags an item with [clipData] payload to the [offset] location.
+     */
+    fun drag(offset: Offset, clipData: ClipData): Boolean
+
+    /**
+     * Drops the previously declared dragging item.
+     */
+    fun drop(): Boolean
+
+    /**
+     * Cancels the ongoing drag without dropping it.
+     */
+    fun cancelDrag()
+}
+
 @Suppress("DELEGATED_MEMBER_HIDES_SUPERTYPE_OVERRIDE")
 private class DragAndDropScopeImpl(
     val view: View,
     density: Density
 ) : DragAndDropScope, Density by density {
-    private var lastDraggingItem: Pair<Offset, Any>? = null
+    private var lastDraggingOffsetAndItem: Pair<Offset, Any>? = null
 
-    override fun drag(
-        offset: Offset,
-        item: Any,
-    ) {
-        val _lastDraggingItem = lastDraggingItem
+    override fun drag(offset: Offset, text: String): Boolean = dragAny(offset, text)
+
+    override fun drag(offset: Offset, uri: Uri): Boolean = dragAny(offset, uri)
+
+    override fun drag(offset: Offset, clipData: ClipData): Boolean = dragAny(offset, clipData)
+
+    /**
+     * @param item Can be only [String], [Uri], or [ClipData].
+     */
+    private fun dragAny(offset: Offset, item: Any): Boolean {
+        val _lastDraggingItem = lastDraggingOffsetAndItem
+        var result = false
         if (_lastDraggingItem == null || _lastDraggingItem.second != item) {
-            view.dispatchDragEvent(
-                makeDragEvent(DragEvent.ACTION_DRAG_STARTED, item)
-            )
+            result = view.dispatchDragEvent(
+                makeDragEvent(action = DragEvent.ACTION_DRAG_STARTED, item = item)
+            ) || result
         }
-        view.dispatchDragEvent(
+        result = view.dispatchDragEvent(
             makeDragEvent(
-                DragEvent.ACTION_DRAG_LOCATION,
+                action = DragEvent.ACTION_DRAG_LOCATION,
                 item = item,
                 offset = offset
             )
-        )
-        lastDraggingItem = offset to item
+        ) || result
+        lastDraggingOffsetAndItem = offset to item
+        return result
     }
 
-    override fun drop() {
-        val _lastDraggingItem = lastDraggingItem
-        check(_lastDraggingItem != null) { "There are no ongoing dragging event to drop" }
+    override fun drop(): Boolean {
+        val lastDraggingOffsetAndItem = lastDraggingOffsetAndItem
+        check(lastDraggingOffsetAndItem != null) { "There are no ongoing dragging event to drop" }
+        val (lastDraggingOffset, lastDraggingItem) = lastDraggingOffsetAndItem
 
-        view.dispatchDragEvent(
+        return view.dispatchDragEvent(
             makeDragEvent(
                 DragEvent.ACTION_DROP,
-                item = _lastDraggingItem.second,
-                offset = _lastDraggingItem.first
+                item = lastDraggingItem,
+                offset = lastDraggingOffset
             )
         )
     }
@@ -92,54 +128,15 @@
                 DragAndDropTestUtils.makeImageDragEvent(action, item, offset)
             }
 
-            is List<*> -> {
-                val mimeTypes = mutableSetOf<String>()
-                val clipDataItems = mutableListOf<ClipData.Item>()
-                item.filterNotNull().forEach { actualItem ->
-                    when (actualItem) {
-                        is String -> {
-                            mimeTypes.add(ClipDescription.MIMETYPE_TEXT_PLAIN)
-                            clipDataItems.add(ClipData.Item(actualItem))
-                        }
-
-                        is Uri -> {
-                            mimeTypes.add("image/*")
-                            clipDataItems.add(ClipData.Item(actualItem))
-                        }
-                    }
-                }
-                DragAndDropTestUtils.makeDragEvent(
-                    action = action,
-                    items = clipDataItems,
-                    mimeTypes = mimeTypes.toList(),
-                    offset = offset
-                )
+            is ClipData -> {
+                DragAndDropTestUtils.makeDragEvent(action, item, offset)
             }
 
             else -> {
-                DragAndDropTestUtils.makeImageDragEvent(action, offset = offset)
+                throw IllegalArgumentException(
+                    "{item=$item} can only be one of [String], [Uri], or [ClipData]"
+                )
             }
         }
     }
 }
-
-internal interface DragAndDropScope : Density {
-
-    /**
-     * Drags an item which represent the payload to the [offset] location.
-     *
-     * @param item Should either be a [String] or a [Uri]. It can also be a [List] of [String]s or
-     * [Uri]s.
-     */
-    fun drag(offset: Offset, item: Any)
-
-    /**
-     * Drops the previously declared dragging item.
-     */
-    fun drop()
-
-    /**
-     * Cancels the ongoing drag without dropping it.
-     */
-    fun cancelDrag()
-}
diff --git a/compose/foundation/foundation/src/androidInstrumentedTest/kotlin/androidx/compose/foundation/lazy/layout/LazyLayoutTest.kt b/compose/foundation/foundation/src/androidInstrumentedTest/kotlin/androidx/compose/foundation/lazy/layout/LazyLayoutTest.kt
index fed28d6..cc9b072 100644
--- a/compose/foundation/foundation/src/androidInstrumentedTest/kotlin/androidx/compose/foundation/lazy/layout/LazyLayoutTest.kt
+++ b/compose/foundation/foundation/src/androidInstrumentedTest/kotlin/androidx/compose/foundation/lazy/layout/LazyLayoutTest.kt
@@ -326,6 +326,40 @@
     }
 
     @Test
+    fun prefetchAndCancelItemWithCustomExecutor() {
+        val itemProvider = itemProvider({ 1 }) { index ->
+            Box(
+                Modifier
+                    .fillMaxSize()
+                    .testTag("$index"))
+        }
+
+        val executor = RecordingPrefetchExecutor()
+        val prefetchState = LazyLayoutPrefetchState(executor)
+        rule.setContent {
+            LazyLayout(itemProvider, prefetchState = prefetchState) {
+                layout(100, 100) {}
+            }
+        }
+
+        val handle = rule.runOnIdle {
+            prefetchState.schedulePrefetch(0, Constraints.fixed(50, 50))
+        }
+
+        assertThat(executor.requests).hasSize(1)
+        assertThat(executor.requests[0].isValid).isTrue()
+
+        // Default PrefetchExecutor behavior should be overridden
+        rule.onNodeWithTag("0").assertDoesNotExist()
+
+        rule.runOnIdle {
+            handle.cancel()
+        }
+
+        assertThat(executor.requests[0].isValid).isFalse()
+    }
+
+    @Test
     fun keptForReuseItemIsDisposedWhenCanceled() {
         val needChild = mutableStateOf(true)
         var composed = true
@@ -546,4 +580,14 @@
         }
         return { provider }
     }
+
+    private class RecordingPrefetchExecutor : PrefetchExecutor {
+
+        private val _requests: MutableList<PrefetchExecutor.Request> = mutableListOf()
+        val requests: List<PrefetchExecutor.Request> = _requests
+
+        override fun requestPrefetch(request: PrefetchExecutor.Request) {
+            _requests.add(request)
+        }
+    }
 }
diff --git a/compose/foundation/foundation/src/androidInstrumentedTest/kotlin/androidx/compose/foundation/lazy/list/LazyListPrefetchStrategyTest.kt b/compose/foundation/foundation/src/androidInstrumentedTest/kotlin/androidx/compose/foundation/lazy/list/LazyListPrefetchStrategyTest.kt
new file mode 100644
index 0000000..94f0250
--- /dev/null
+++ b/compose/foundation/foundation/src/androidInstrumentedTest/kotlin/androidx/compose/foundation/lazy/list/LazyListPrefetchStrategyTest.kt
@@ -0,0 +1,304 @@
+/*
+ * 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.lazy.list
+
+import androidx.compose.foundation.ExperimentalFoundationApi
+import androidx.compose.foundation.gestures.Orientation
+import androidx.compose.foundation.gestures.scrollBy
+import androidx.compose.foundation.layout.Spacer
+import androidx.compose.foundation.lazy.DefaultLazyListPrefetchStrategy
+import androidx.compose.foundation.lazy.LazyListLayoutInfo
+import androidx.compose.foundation.lazy.LazyListPrefetchScope
+import androidx.compose.foundation.lazy.LazyListPrefetchStrategy
+import androidx.compose.foundation.lazy.LazyListState
+import androidx.compose.foundation.lazy.layout.LazyLayoutPrefetchState
+import androidx.compose.foundation.lazy.list.LazyListPrefetchStrategyTest.RecordingLazyListPrefetchStrategy.Callback
+import androidx.compose.foundation.lazy.rememberLazyListState
+import androidx.compose.runtime.DisposableEffect
+import androidx.compose.runtime.MutableState
+import androidx.compose.runtime.mutableStateOf
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.layout.layout
+import androidx.compose.ui.platform.testTag
+import androidx.compose.ui.test.onNodeWithTag
+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)
+@OptIn(ExperimentalFoundationApi::class)
+class LazyListPrefetchStrategyTest(
+    val config: Config
+) : BaseLazyListTestWithOrientation(config.orientation) {
+
+    companion object {
+        @JvmStatic
+        @Parameterized.Parameters(name = "{0}")
+        fun initParameters(): Array<Any> = arrayOf(
+            Config(Orientation.Vertical),
+            Config(Orientation.Horizontal),
+        )
+
+        class Config(
+            val orientation: Orientation,
+        ) {
+            override fun toString() = "orientation=$orientation"
+        }
+
+        private val LazyListLayoutInfo.visibleIndices: List<Int>
+            get() = visibleItemsInfo.map { it.index }.sorted()
+    }
+
+    private val itemsSizePx = 30
+    private val itemsSizeDp = with(rule.density) { itemsSizePx.toDp() }
+
+    lateinit var state: LazyListState
+
+    @Test
+    fun callbacksTriggered_whenScrollForwardsWithoutVisibleItemsChanged() {
+        val strategy = RecordingLazyListPrefetchStrategy()
+
+        composeList(prefetchStrategy = strategy)
+
+        assertThat(strategy.callbacks).containsExactly(
+            Callback.OnVisibleItemsUpdated(
+                visibleIndices = listOf(0, 1)
+            ),
+        ).inOrder()
+        strategy.reset()
+
+        rule.runOnIdle {
+            runBlocking {
+                state.scrollBy(5f)
+            }
+        }
+
+        assertThat(strategy.callbacks).containsExactly(
+            Callback.OnScroll(
+                delta = -5f,
+                visibleIndices = listOf(0, 1)
+            ),
+        ).inOrder()
+    }
+
+    @Test
+    fun callbacksTriggered_whenScrollBackwardsWithoutVisibleItemsChanged() {
+        val strategy = RecordingLazyListPrefetchStrategy()
+
+        composeList(firstItem = 10, itemOffset = 10, prefetchStrategy = strategy)
+
+        assertThat(strategy.callbacks).containsExactly(
+            Callback.OnVisibleItemsUpdated(
+                visibleIndices = listOf(10, 11)
+            ),
+        ).inOrder()
+        strategy.reset()
+
+        rule.runOnIdle {
+            runBlocking {
+                state.scrollBy(-5f)
+            }
+        }
+
+        assertThat(strategy.callbacks).containsExactly(
+            Callback.OnScroll(
+                delta = 5f,
+                visibleIndices = listOf(10, 11)
+            ),
+        ).inOrder()
+    }
+
+    @Test
+    fun callbacksTriggered_whenScrollWithVisibleItemsChanged() {
+        val strategy = RecordingLazyListPrefetchStrategy()
+
+        composeList(prefetchStrategy = strategy)
+
+        assertThat(strategy.callbacks).containsExactly(
+            Callback.OnVisibleItemsUpdated(
+                visibleIndices = listOf(0, 1)
+            ),
+        ).inOrder()
+        strategy.reset()
+
+        rule.runOnIdle {
+            runBlocking {
+                state.scrollBy(itemsSizePx + 5f)
+            }
+        }
+
+        assertThat(strategy.callbacks).containsExactly(
+            Callback.OnVisibleItemsUpdated(
+                visibleIndices = listOf(1, 2)
+            ),
+            Callback.OnScroll(
+                delta = -(itemsSizePx + 5f),
+                visibleIndices = listOf(1, 2)
+            ),
+        ).inOrder()
+    }
+
+    @Test
+    fun callbacksTriggered_whenItemsChangedWithoutScroll() {
+        val strategy = RecordingLazyListPrefetchStrategy()
+        val numItems = mutableStateOf(100)
+
+        composeList(prefetchStrategy = strategy, numItems = numItems)
+
+        assertThat(strategy.callbacks).containsExactly(
+            Callback.OnVisibleItemsUpdated(
+                visibleIndices = listOf(0, 1)
+            ),
+        ).inOrder()
+        strategy.reset()
+
+        numItems.value = 1
+
+        rule.waitForIdle()
+
+        assertThat(strategy.callbacks).containsExactly(
+            Callback.OnVisibleItemsUpdated(
+                visibleIndices = listOf(0)
+            ),
+        ).inOrder()
+    }
+
+    @Test
+    fun itemComposed_whenPrefetchedFromCallback() {
+        val strategy = PrefetchNextLargestIndexStrategy()
+
+        composeList(prefetchStrategy = strategy)
+
+        rule.runOnIdle {
+            runBlocking {
+                state.scrollBy(5f)
+            }
+        }
+
+        waitForPrefetch(2)
+        rule.onNodeWithTag("2")
+            .assertExists()
+    }
+
+    private fun waitForPrefetch(index: Int) {
+        rule.waitUntil {
+            activeNodes.contains(index) && activeMeasuredNodes.contains(index)
+        }
+    }
+
+    private val activeNodes = mutableSetOf<Int>()
+    private val activeMeasuredNodes = mutableSetOf<Int>()
+
+    @OptIn(ExperimentalFoundationApi::class)
+    private fun composeList(
+        firstItem: Int = 0,
+        itemOffset: Int = 0,
+        numItems: MutableState<Int> = mutableStateOf(100),
+        prefetchStrategy: LazyListPrefetchStrategy = DefaultLazyListPrefetchStrategy()
+    ) {
+        rule.setContent {
+            state = rememberLazyListState(
+                initialFirstVisibleItemIndex = firstItem,
+                initialFirstVisibleItemScrollOffset = itemOffset,
+                prefetchStrategy = prefetchStrategy
+            )
+            LazyColumnOrRow(
+                Modifier.mainAxisSize(itemsSizeDp * 1.5f),
+                state,
+            ) {
+                items(numItems.value) {
+                    DisposableEffect(it) {
+                        activeNodes.add(it)
+                        onDispose {
+                            activeNodes.remove(it)
+                            activeMeasuredNodes.remove(it)
+                        }
+                    }
+                    Spacer(
+                        Modifier
+                            .mainAxisSize(itemsSizeDp)
+                            .fillMaxCrossAxis()
+                            .testTag("$it")
+                            .layout { measurable, constraints ->
+                                val placeable = measurable.measure(constraints)
+                                activeMeasuredNodes.add(it)
+                                layout(placeable.width, placeable.height) {
+                                    placeable.place(0, 0)
+                                }
+                            }
+                    )
+                }
+            }
+        }
+    }
+
+    /**
+     * LazyListPrefetchStrategy that just records callbacks without scheduling prefetches.
+     */
+    private class RecordingLazyListPrefetchStrategy : LazyListPrefetchStrategy {
+        sealed interface Callback {
+            data class OnScroll(val delta: Float, val visibleIndices: List<Int>) : Callback
+            data class OnVisibleItemsUpdated(val visibleIndices: List<Int>) : Callback
+        }
+
+        private val _callbacks: MutableList<Callback> = mutableListOf()
+        val callbacks: List<Callback> = _callbacks
+
+        override fun LazyListPrefetchScope.onScroll(delta: Float, layoutInfo: LazyListLayoutInfo) {
+            _callbacks.add(Callback.OnScroll(delta, layoutInfo.visibleIndices))
+        }
+
+        override fun LazyListPrefetchScope.onVisibleItemsUpdated(layoutInfo: LazyListLayoutInfo) {
+            _callbacks.add(Callback.OnVisibleItemsUpdated(layoutInfo.visibleIndices))
+        }
+
+        fun reset() {
+            _callbacks.clear()
+        }
+    }
+
+    /**
+     * LazyListPrefetchStrategy that always prefetches the next largest index off screen no matter
+     * the scroll direction.
+     */
+    private class PrefetchNextLargestIndexStrategy : LazyListPrefetchStrategy {
+
+        private var handle: LazyLayoutPrefetchState.PrefetchHandle? = null
+        private var prefetchIndex: Int = -1
+
+        override fun LazyListPrefetchScope.onScroll(delta: Float, layoutInfo: LazyListLayoutInfo) {
+            val index = layoutInfo.visibleIndices.last() + 1
+            if (handle != null && index != prefetchIndex) {
+                cancelPrefetch()
+            }
+            handle = schedulePrefetch(index)
+            prefetchIndex = index
+        }
+
+        override fun LazyListPrefetchScope.onVisibleItemsUpdated(layoutInfo: LazyListLayoutInfo) =
+            Unit
+
+        private fun cancelPrefetch() {
+            handle?.cancel()
+            prefetchIndex = -1
+        }
+    }
+}
diff --git a/compose/foundation/foundation/src/androidInstrumentedTest/kotlin/androidx/compose/foundation/lazy/list/LazyListTest.kt b/compose/foundation/foundation/src/androidInstrumentedTest/kotlin/androidx/compose/foundation/lazy/list/LazyListTest.kt
index df9f74f..da3f43f 100644
--- a/compose/foundation/foundation/src/androidInstrumentedTest/kotlin/androidx/compose/foundation/lazy/list/LazyListTest.kt
+++ b/compose/foundation/foundation/src/androidInstrumentedTest/kotlin/androidx/compose/foundation/lazy/list/LazyListTest.kt
@@ -41,6 +41,7 @@
 import androidx.compose.foundation.layout.requiredSizeIn
 import androidx.compose.foundation.layout.requiredWidth
 import androidx.compose.foundation.layout.size
+import androidx.compose.foundation.layout.wrapContentSize
 import androidx.compose.foundation.lazy.LazyItemScope
 import androidx.compose.foundation.lazy.LazyListState
 import androidx.compose.foundation.lazy.items
@@ -2704,6 +2705,111 @@
         }
     }
 
+    @Test
+    fun animContentSize_resetOnReuse() = with(rule.density) {
+        val visBoxCount = 10
+        val maxItemCount = 10 * visBoxCount
+        val boxHeightPx = 100
+        val smallBoxPx = 100
+        val mediumBoxPx = 200
+        val largeBoxPx = 300
+        val maxHeightPx = visBoxCount * boxHeightPx
+
+        val mediumItemsStartIndex = 50
+
+        var assertedSmallItems = false
+        var assertedMediumItems = false
+
+        /**
+         * Since only the first item is expected to animate. This Modifier can check that all other
+         * items were only measure at their expected dimensions.
+         */
+        fun Modifier.assertMeasureCalls(index: Int): Modifier {
+            return this.layout { measurable, constraints ->
+                val placeable = measurable.measure(constraints)
+                when {
+                    index == 0 -> {
+                        // Do nothing
+                    }
+
+                    index >= mediumItemsStartIndex -> {
+                        assertedMediumItems = true
+                        assertEquals(mediumBoxPx, placeable.width)
+                        assertEquals(boxHeightPx, placeable.height)
+                    }
+
+                    else -> {
+                        assertedSmallItems = true
+                        assertEquals(smallBoxPx, placeable.width)
+                        assertEquals(boxHeightPx, placeable.height)
+                    }
+                }
+                layout(placeable.width, placeable.height) {
+                    placeable.place(0, 0)
+                }
+            }
+        }
+
+        val isFirstElementExpanded = mutableStateOf(false)
+
+        val listState = LazyListState()
+        rule.setContent {
+            LazyColumnOrRow(
+                modifier = Modifier.height(maxHeightPx.toDp()),
+                state = listState
+            ) {
+                items(maxItemCount) { index ->
+                    val modifier = when {
+                        index == 0 -> {
+                            val sizeDp = if (isFirstElementExpanded.value) {
+                                largeBoxPx.toDp()
+                            } else {
+                                smallBoxPx.toDp()
+                            }
+                            Modifier.requiredSize(sizeDp)
+                        }
+
+                        index >= mediumItemsStartIndex -> {
+                            Modifier
+                                .requiredSize(mediumBoxPx.toDp(), boxHeightPx.toDp())
+                        }
+
+                        else -> {
+                            Modifier
+                                .requiredSize(smallBoxPx.toDp(), boxHeightPx.toDp())
+                        }
+                    }
+                    Box(
+                        Modifier
+                            .wrapContentSize()
+                            .assertMeasureCalls(index)
+                            .animateContentSize()
+                    ) {
+                        Box(modifier)
+                    }
+                }
+            }
+        }
+
+        // Wait for layout to settle
+        rule.waitForIdle()
+
+        // Trigger an animation on the first element and wait for completion
+        isFirstElementExpanded.value = true
+        rule.waitForIdle()
+
+        // Scroll to a different index and wait for layout to settle.
+        // At the selected index, ALL layout nodes will have a different size. If
+        // `animateContentSize` wasn't properly reset, it might have started more animations, caught
+        // with `assertMeasureCalls`.
+        listState.scrollTo(mediumItemsStartIndex)
+        rule.waitForIdle()
+
+        // Safety check to prevent silent failures (no UI was run)
+        assert(assertedMediumItems)
+        assert(assertedSmallItems)
+    }
+
     @OptIn(ExperimentalComposeUiApi::class, ExperimentalFoundationApi::class)
     @Test
     fun animVisibilityWithPlacementAnimator() {
diff --git a/compose/foundation/foundation/src/androidInstrumentedTest/kotlin/androidx/compose/foundation/lazy/staggeredgrid/LazyStaggeredGridTest.kt b/compose/foundation/foundation/src/androidInstrumentedTest/kotlin/androidx/compose/foundation/lazy/staggeredgrid/LazyStaggeredGridTest.kt
index 1cf1414..a376481 100644
--- a/compose/foundation/foundation/src/androidInstrumentedTest/kotlin/androidx/compose/foundation/lazy/staggeredgrid/LazyStaggeredGridTest.kt
+++ b/compose/foundation/foundation/src/androidInstrumentedTest/kotlin/androidx/compose/foundation/lazy/staggeredgrid/LazyStaggeredGridTest.kt
@@ -25,6 +25,7 @@
 import androidx.compose.foundation.layout.BoxWithConstraints
 import androidx.compose.foundation.layout.Spacer
 import androidx.compose.foundation.layout.size
+import androidx.compose.foundation.lazy.list.assertIsPlaced
 import androidx.compose.foundation.lazy.list.setContentWithTestViewConfiguration
 import androidx.compose.foundation.text.BasicText
 import androidx.compose.runtime.DisposableEffect
@@ -50,6 +51,7 @@
 import androidx.compose.ui.unit.dp
 import androidx.test.filters.MediumTest
 import com.google.common.truth.Truth.assertThat
+import kotlinx.coroutines.Dispatchers
 import kotlinx.coroutines.runBlocking
 import org.junit.After
 import org.junit.Assert.assertTrue
@@ -2170,4 +2172,102 @@
             assertThat(composedItems).isEqualTo(setOf(0, 1, 2, 3))
         }
     }
+
+    @Test
+    fun zeroSizeItemIsPlacedWhenItIsAtTheTop() {
+        lateinit var state: LazyStaggeredGridState
+
+        rule.setContent {
+            state = rememberLazyStaggeredGridState(initialFirstVisibleItemIndex = 0)
+            LazyStaggeredGrid(
+                lanes = 2,
+                state = state,
+                modifier = Modifier
+                    .mainAxisSize(itemSizeDp * 2)
+                    .crossAxisSize(itemSizeDp * 2)
+            ) {
+                repeat(10) { index ->
+                    items(2) {
+                        Spacer(Modifier.testTag("${index * 10 + it}"))
+                    }
+                    items(8) {
+                        Spacer(Modifier.mainAxisSize(itemSizeDp))
+                    }
+                }
+            }
+        }
+
+        rule.onNodeWithTag("0")
+            .assertIsPlaced()
+            .assertMainAxisStartPositionInRootIsEqualTo(0.dp)
+            .assertMainAxisSizeIsEqualTo(0.dp)
+
+        rule.onNodeWithTag("1")
+            .assertIsPlaced()
+            .assertMainAxisStartPositionInRootIsEqualTo(0.dp)
+            .assertMainAxisSizeIsEqualTo(0.dp)
+
+        runBlocking(Dispatchers.Main + AutoTestFrameClock()) {
+            state.scrollToItem(10, 0)
+        }
+
+        rule.onNodeWithTag("10")
+            .assertIsPlaced()
+            .assertMainAxisStartPositionInRootIsEqualTo(0.dp)
+            .assertMainAxisSizeIsEqualTo(0.dp)
+
+        rule.onNodeWithTag("11")
+            .assertIsPlaced()
+            .assertMainAxisStartPositionInRootIsEqualTo(0.dp)
+            .assertMainAxisSizeIsEqualTo(0.dp)
+    }
+
+    @Test
+    fun itemsAreDistributedCorrectlyOnOverscrollPassWithSameOffset() {
+        val gridHeight = itemSizeDp * 11 // two big items + two small items
+        state = LazyStaggeredGridState()
+        rule.setContent {
+            LazyStaggeredGrid(
+                modifier = Modifier
+                    .mainAxisSize(gridHeight)
+                    .crossAxisSize(itemSizeDp * 2),
+                state = state,
+                lanes = 2,
+            ) {
+                items(20) {
+                    Spacer(
+                        Modifier
+                            .mainAxisSize(if (it % 2 == 0) itemSizeDp * 5 else itemSizeDp * 0.5f)
+                            .border(1.dp, Color.Red)
+                            .testTag("$it")
+                    )
+                }
+            }
+        }
+
+        // scroll to bottom
+        state.scrollBy(gridHeight * 2)
+
+        rule.onNodeWithTag("12")
+            .assertCrossAxisStartPositionInRootIsEqualTo(0.dp)
+            .assertMainAxisStartPositionInRootIsEqualTo(0.dp)
+
+        rule.onNodeWithTag("13")
+            .assertCrossAxisStartPositionInRootIsEqualTo(itemSizeDp)
+            .assertMainAxisStartPositionInRootIsEqualTo(0.dp)
+
+        // scroll a back a bit
+        state.scrollBy(-itemSizeDp * 5)
+
+        // scroll by a grid height
+        state.scrollBy(gridHeight)
+
+        rule.onNodeWithTag("12")
+            .assertCrossAxisStartPositionInRootIsEqualTo(0.dp)
+            .assertMainAxisStartPositionInRootIsEqualTo(0.dp)
+
+        rule.onNodeWithTag("13")
+            .assertCrossAxisStartPositionInRootIsEqualTo(itemSizeDp)
+            .assertMainAxisStartPositionInRootIsEqualTo(0.dp)
+    }
 }
diff --git a/compose/foundation/foundation/src/androidInstrumentedTest/kotlin/androidx/compose/foundation/text/BasicTextLinkTest.kt b/compose/foundation/foundation/src/androidInstrumentedTest/kotlin/androidx/compose/foundation/text/BasicTextLinkTest.kt
index f22a59c..a7af694 100644
--- a/compose/foundation/foundation/src/androidInstrumentedTest/kotlin/androidx/compose/foundation/text/BasicTextLinkTest.kt
+++ b/compose/foundation/foundation/src/androidInstrumentedTest/kotlin/androidx/compose/foundation/text/BasicTextLinkTest.kt
@@ -46,6 +46,8 @@
 import androidx.compose.ui.platform.ViewConfiguration
 import androidx.compose.ui.platform.testTag
 import androidx.compose.ui.semantics.SemanticsActions
+import androidx.compose.ui.semantics.SemanticsProperties
+import androidx.compose.ui.test.SemanticsMatcher
 import androidx.compose.ui.test.SemanticsNodeInteraction
 import androidx.compose.ui.test.SemanticsNodeInteractionsProvider
 import androidx.compose.ui.test.assertIsFocused
@@ -115,7 +117,7 @@
             focusManager.moveFocus(FocusDirection.Previous)
         }
 
-        rule.onAllNodes(hasClickAction())[2].assertIsFocused()
+        rule.onAllNodes(hasClickAction(), useUnmergedTree = true)[2].assertIsFocused()
     }
 
     @Test
@@ -128,8 +130,8 @@
             focusManager.moveFocus(FocusDirection.Previous)
         }
 
-        rule.onAllNodes(hasClickAction())[2].assertIsNotFocused()
-        rule.onAllNodes(hasClickAction())[1].assertIsFocused()
+        rule.onAllNodes(hasClickAction(), useUnmergedTree = true)[2].assertIsNotFocused()
+        rule.onAllNodes(hasClickAction(), useUnmergedTree = true)[1].assertIsFocused()
     }
 
     @Test
@@ -143,9 +145,9 @@
             focusManager.moveFocus(FocusDirection.Previous)
         }
 
-        rule.onAllNodes(hasClickAction())[2].assertIsNotFocused()
-        rule.onAllNodes(hasClickAction())[1].assertIsNotFocused()
-        rule.onAllNodes(hasClickAction())[0].assertIsFocused()
+        rule.onAllNodes(hasClickAction(), useUnmergedTree = true)[2].assertIsNotFocused()
+        rule.onAllNodes(hasClickAction(), useUnmergedTree = true)[1].assertIsNotFocused()
+        rule.onAllNodes(hasClickAction(), useUnmergedTree = true)[0].assertIsFocused()
     }
 
     @Test
@@ -222,6 +224,96 @@
     }
 
     @Test
+    fun rtlText_onClick_insideFirstLink_opensFirstUrl() {
+        setupContent { RtlTextWithLinks() }
+
+        rule.runOnIdle { assertThat(layoutResult).isNotNull() }
+        rule.onNode(SemanticsMatcher.keyIsDefined(SemanticsProperties.Text)).performTouchInput {
+            val boundingBox = layoutResult!!.getBoundingBox(3)
+            click(boundingBox.center)
+        }
+
+        rule.runOnIdle {
+            assertThat(openedUri).isEqualTo(Url1)
+        }
+    }
+
+    @Test
+    fun rtlText_onClick_insideSecondLink_opensSecondUrl() {
+        setupContent { RtlTextWithLinks() }
+
+        rule.runOnIdle { assertThat(layoutResult).isNotNull() }
+        rule.onNode(SemanticsMatcher.keyIsDefined(SemanticsProperties.Text)).performTouchInput {
+            val boundingBox = layoutResult!!.getBoundingBox(30)
+            click(boundingBox.center)
+        }
+
+        rule.runOnIdle {
+            assertThat(openedUri).isEqualTo(Url2)
+        }
+    }
+
+    @Test
+    fun rtlText_onClick_outsideLink_doNothing() {
+        setupContent { RtlTextWithLinks() }
+
+        rule.runOnIdle { assertThat(layoutResult).isNotNull() }
+        rule.onNode(SemanticsMatcher.keyIsDefined(SemanticsProperties.Text)).performTouchInput {
+            val boundingBox = layoutResult!!.getBoundingBox(35)
+            click(boundingBox.center)
+        }
+
+        rule.runOnIdle {
+            assertThat(openedUri).isEqualTo(null)
+        }
+    }
+
+    @Test
+    fun rtlText_onClick_inBetweenLinks_doNothing() {
+        setupContent { RtlTextWithLinks() }
+
+        rule.runOnIdle { assertThat(layoutResult).isNotNull() }
+        rule.onNode(SemanticsMatcher.keyIsDefined(SemanticsProperties.Text)).performTouchInput {
+            val boundingBox = layoutResult!!.getBoundingBox(20)
+            click(boundingBox.center)
+        }
+
+        rule.runOnIdle {
+            assertThat(openedUri).isEqualTo(null)
+        }
+    }
+
+    @Test
+    fun bidiText_onClick_insideLink_opensUrl() {
+        setupContent { BidiTextWithLinks() }
+
+        rule.runOnIdle { assertThat(layoutResult).isNotNull() }
+        rule.onNode(SemanticsMatcher.keyIsDefined(SemanticsProperties.Text)).performTouchInput {
+            val boundingBox = layoutResult!!.getBoundingBox(8)
+            click(boundingBox.center)
+        }
+
+        rule.runOnIdle {
+            assertThat(openedUri).isEqualTo(Url1)
+        }
+    }
+
+    @Test
+    fun bidiText_onClick_outsideLink_doNothing() {
+        setupContent { BidiTextWithLinks() }
+
+        rule.runOnIdle { assertThat(layoutResult).isNotNull() }
+        rule.onNode(SemanticsMatcher.keyIsDefined(SemanticsProperties.Text)).performTouchInput {
+            val boundingBox = layoutResult!!.getBoundingBox(2)
+            click(boundingBox.center)
+        }
+
+        rule.runOnIdle {
+            assertThat(openedUri).isEqualTo(null)
+        }
+    }
+
+    @Test
     fun link_andInlineContent_onClick_opensUrl() {
         setupContent {
             /***
@@ -254,9 +346,9 @@
             )
         }
 
-        rule.onAllNodes(hasClickAction())[0].performClick()
+        rule.onAllNodes(hasClickAction(), useUnmergedTree = true)[0].performClick()
 
-        rule.onNodeWithTag("box").assertExists()
+        rule.onNodeWithTag("box", useUnmergedTree = true).assertExists()
         rule.runOnIdle {
             assertThat(openedUri).isEqualTo(Url1)
         }
@@ -327,7 +419,7 @@
         }
 
         rule.onNodeWithTag("box").assertIsNotFocused()
-        rule.onNode(hasClickAction()).assertIsFocused()
+        rule.onNode(hasClickAction(), useUnmergedTree = true).assertIsFocused()
     }
 
     @Test
@@ -440,6 +532,29 @@
         }
     }
 
+    @Composable
+    private fun BidiTextWithLinks() {
+        val text = buildAnnotatedString {
+            append("\u05D0\u05D1 \u05D2\u05D3")
+            withAnnotation(Url(Url1)) { append(" text ") }
+            append("\u05D0\u05D1 \u05D2\u05D3 \u05D0\u05D1 \u05D2\u05D3")
+        }
+        BasicText(text, onTextLayout = { layoutResult = it })
+    }
+
+    @Composable
+    private fun RtlTextWithLinks() {
+        val text = buildAnnotatedString {
+            withAnnotation(Url(Url1)) { append("\u05D0\u05D1 \u05D2\u05D3 \u05D0\u05D1") }
+            append(" \u05D0\u05D1 \u05D2\u05D3 \u05D0\u05D1 \u05D0\u05D1 \u05D2\u05D3")
+            withAnnotation(Url(Url2)) {
+                append(" \u05D0\u05D1 \u05D2\u05D3 \u05D0\u05D1")
+            }
+            append("\u05D0\u05D1 \u05D2\u05D3")
+        }
+        BasicText(text, onTextLayout = { layoutResult = it })
+    }
+
     @OptIn(ExperimentalComposeUiApi::class)
     private fun setupContent(content: @Composable () -> Unit) {
         val keyboardMockManager = object : InputModeManager {
diff --git a/compose/foundation/foundation/src/androidInstrumentedTest/kotlin/androidx/compose/foundation/text/BasicTextSemanticsTest.kt b/compose/foundation/foundation/src/androidInstrumentedTest/kotlin/androidx/compose/foundation/text/BasicTextSemanticsTest.kt
index 7765353..82097da 100644
--- a/compose/foundation/foundation/src/androidInstrumentedTest/kotlin/androidx/compose/foundation/text/BasicTextSemanticsTest.kt
+++ b/compose/foundation/foundation/src/androidInstrumentedTest/kotlin/androidx/compose/foundation/text/BasicTextSemanticsTest.kt
@@ -16,14 +16,25 @@
 
 package androidx.compose.foundation.text
 
+import androidx.compose.foundation.ExperimentalFoundationApi
 import androidx.compose.runtime.getValue
 import androidx.compose.runtime.mutableStateOf
 import androidx.compose.runtime.setValue
+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.junit4.createComposeRule
+import androidx.compose.ui.test.onChildAt
 import androidx.compose.ui.test.onNodeWithText
 import androidx.compose.ui.text.AnnotatedString
+import androidx.compose.ui.text.LinkAnnotation
+import androidx.compose.ui.text.TextRange
+import androidx.compose.ui.text.buildAnnotatedString
+import androidx.compose.ui.text.withAnnotation
 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
@@ -56,4 +67,41 @@
         text = "after"
         rule.onNodeWithText("after").assertExists()
     }
+
+    @OptIn(ExperimentalFoundationApi::class)
+    @Test
+    fun link_semantics_AnnotatedString() {
+        rule.setContent {
+            BasicText(
+                text = buildAnnotatedString {
+                    withAnnotation(LinkAnnotation.Url("url")) { append("abc") }
+                    withAnnotation(LinkAnnotation.Clickable("tag")) { append("def") }
+                },
+                onLinkClicked = {}
+            )
+        }
+
+        val node = rule
+            .onNodeWithText("abcdef", useUnmergedTree = true)
+            .assertExists()
+            .fetchSemanticsNode()
+        assertThat(node.children.size).isEqualTo(2)
+        assertThat(node.config.isClearingSemantics).isTrue()
+
+        rule.onNodeWithText("abcdef", useUnmergedTree = true)
+            .onChildAt(0)
+            .assert(SemanticsMatcher.keyIsDefined(SemanticsActions.CustomActions))
+            .assert(SemanticsMatcher.expectValue(
+                SemanticsProperties.TextSelectionRange,
+                TextRange(0, 3)
+            ))
+
+        rule.onNodeWithText("abcdef", useUnmergedTree = true)
+            .onChildAt(1)
+            .assert(SemanticsMatcher.keyIsDefined(SemanticsActions.CustomActions))
+            .assert(SemanticsMatcher.expectValue(
+                SemanticsProperties.TextSelectionRange,
+                TextRange(3, 6)
+            ))
+    }
 }
diff --git a/compose/foundation/foundation/src/androidInstrumentedTest/kotlin/androidx/compose/foundation/text/CoreTextInlineContentTest.kt b/compose/foundation/foundation/src/androidInstrumentedTest/kotlin/androidx/compose/foundation/text/CoreTextInlineContentTest.kt
index 63c6bc8..aa22933 100644
--- a/compose/foundation/foundation/src/androidInstrumentedTest/kotlin/androidx/compose/foundation/text/CoreTextInlineContentTest.kt
+++ b/compose/foundation/foundation/src/androidInstrumentedTest/kotlin/androidx/compose/foundation/text/CoreTextInlineContentTest.kt
@@ -220,7 +220,9 @@
     }
 
     private fun expectInlineContentPosition(left: Int, right: Int) {
-        val (boxLeft, boxRight) = with(rule.onNodeWithTag("box").fetchSemanticsNode()) {
+        val (boxLeft, boxRight) = with(
+            rule.onNodeWithTag("box", useUnmergedTree = true).fetchSemanticsNode()
+        ) {
             Pair(positionInRoot.x, positionInRoot.x + size.width)
         }
         val (textLeft, textRight) = with(rule.onNodeWithTag("text").fetchSemanticsNode()) {
diff --git a/compose/foundation/foundation/src/androidInstrumentedTest/kotlin/androidx/compose/foundation/text2/input/BasicTextField2Test.kt b/compose/foundation/foundation/src/androidInstrumentedTest/kotlin/androidx/compose/foundation/text2/input/BasicTextField2Test.kt
index f103123..a6fff29 100644
--- a/compose/foundation/foundation/src/androidInstrumentedTest/kotlin/androidx/compose/foundation/text2/input/BasicTextField2Test.kt
+++ b/compose/foundation/foundation/src/androidInstrumentedTest/kotlin/androidx/compose/foundation/text2/input/BasicTextField2Test.kt
@@ -34,6 +34,7 @@
 import androidx.compose.foundation.text.selection.fetchTextLayoutResult
 import androidx.compose.foundation.text2.BasicTextField2
 import androidx.compose.foundation.text2.input.TextFieldBuffer.ChangeList
+import androidx.compose.foundation.text2.input.internal.selection.FakeClipboardManager
 import androidx.compose.foundation.verticalScroll
 import androidx.compose.runtime.CompositionLocalProvider
 import androidx.compose.runtime.LaunchedEffect
@@ -106,10 +107,16 @@
 internal class BasicTextField2Test {
     @get:Rule
     val rule = createComposeRule()
+
+    @get:Rule
+    val immRule = ComposeInputMethodManagerTestRule()
+
     private val inputMethodInterceptor = InputMethodInterceptor(rule)
 
     private val Tag = "BasicTextField2"
 
+    private val imm = FakeInputMethodManager()
+
     @Test
     fun textField_rendersEmptyContent() {
         var textLayoutResult: (() -> TextLayoutResult?)? = null
@@ -453,7 +460,10 @@
             focusManager = LocalFocusManager.current
             Row {
                 // Extra focusable that takes initial focus when focus is cleared.
-                Box(Modifier.size(10.dp).focusable())
+                Box(
+                    Modifier
+                        .size(10.dp)
+                        .focusable())
                 BasicTextField2(
                     state = state,
                     modifier = Modifier.testTag("TextField")
@@ -1301,6 +1311,103 @@
         assertThat(tfs.text.selectionInChars).isEqualTo(TextRange(longText.length))
     }
 
+    @Test
+    fun selectAll_contextMenuAction_informsImeOfSelectionChange() {
+        immRule.setFactory { imm }
+        val state = TextFieldState("Hello")
+        inputMethodInterceptor.setTextFieldTestContent {
+            BasicTextField2(
+                state = state,
+                modifier = Modifier.testTag(Tag)
+            )
+        }
+
+        requestFocus(Tag)
+
+        inputMethodInterceptor.withInputConnection {
+            performContextMenuAction(android.R.id.selectAll)
+        }
+
+        rule.runOnIdle {
+            assertThat(state.text.selectionInChars).isEqualTo(TextRange(0, 5))
+            assertThat(imm.expectCall("updateSelection(0, 5, -1, -1)"))
+        }
+    }
+
+    @Test
+    fun cut_contextMenuAction_cutsIntoClipboard() {
+        val clipboardManager = FakeClipboardManager("World")
+        val state = TextFieldState("Hello", initialSelectionInChars = TextRange(0, 2))
+        inputMethodInterceptor.setTextFieldTestContent {
+            CompositionLocalProvider(LocalClipboardManager provides clipboardManager) {
+                BasicTextField2(
+                    state = state,
+                    modifier = Modifier.testTag(Tag)
+                )
+            }
+        }
+
+        requestFocus(Tag)
+
+        inputMethodInterceptor.withInputConnection {
+            performContextMenuAction(android.R.id.cut)
+        }
+
+        rule.runOnIdle {
+            assertThat(clipboardManager.getText()?.text).isEqualTo("He")
+            assertThat(state.text.toString()).isEqualTo("llo")
+        }
+    }
+
+    @Test
+    fun copy_contextMenuAction_copiesIntoClipboard() {
+        val clipboardManager = FakeClipboardManager("World")
+        val state = TextFieldState("Hello", initialSelectionInChars = TextRange(0, 2))
+        inputMethodInterceptor.setTextFieldTestContent {
+            CompositionLocalProvider(LocalClipboardManager provides clipboardManager) {
+                BasicTextField2(
+                    state = state,
+                    modifier = Modifier.testTag(Tag)
+                )
+            }
+        }
+
+        requestFocus(Tag)
+
+        inputMethodInterceptor.withInputConnection {
+            performContextMenuAction(android.R.id.copy)
+        }
+
+        rule.runOnIdle {
+            assertThat(clipboardManager.getText()?.text).isEqualTo("He")
+        }
+    }
+
+    @Test
+    fun paste_contextMenuAction_pastesFromClipboard() {
+        val clipboardManager = FakeClipboardManager("World")
+        val state = TextFieldState("Hello", initialSelectionInChars = TextRange(0, 4))
+        inputMethodInterceptor.setTextFieldTestContent {
+            CompositionLocalProvider(LocalClipboardManager provides clipboardManager) {
+                BasicTextField2(
+                    state = state,
+                    modifier = Modifier.testTag(Tag)
+                )
+            }
+        }
+
+        requestFocus(Tag)
+
+        inputMethodInterceptor.withInputConnection {
+            performContextMenuAction(android.R.id.paste)
+        }
+
+        rule.runOnIdle {
+            assertThat(state.text.toString()).isEqualTo("Worldo")
+            assertThat(state.text.selectionInChars).isEqualTo(TextRange(5))
+        }
+    }
+
     private fun requestFocus(tag: String) =
         rule.onNodeWithTag(tag).requestFocus()
 
diff --git a/compose/foundation/foundation/src/androidInstrumentedTest/kotlin/androidx/compose/foundation/text2/input/TextFieldCodepointTransformationTest.kt b/compose/foundation/foundation/src/androidInstrumentedTest/kotlin/androidx/compose/foundation/text2/input/TextFieldCodepointTransformationTest.kt
index 7c98465..8ca4a6a1 100644
--- a/compose/foundation/foundation/src/androidInstrumentedTest/kotlin/androidx/compose/foundation/text2/input/TextFieldCodepointTransformationTest.kt
+++ b/compose/foundation/foundation/src/androidInstrumentedTest/kotlin/androidx/compose/foundation/text2/input/TextFieldCodepointTransformationTest.kt
@@ -39,6 +39,7 @@
 import androidx.compose.ui.test.withKeyDown
 import androidx.compose.ui.text.TextRange
 import androidx.test.ext.junit.runners.AndroidJUnit4
+import androidx.test.filters.FlakyTest
 import androidx.test.filters.MediumTest
 import com.google.common.truth.Truth.assertThat
 import com.google.common.truth.Truth.assertWithMessage
@@ -351,6 +352,7 @@
         )
     }
 
+    @FlakyTest(bugId = 317749301)
     @Test
     fun multipleCodepoints_selectionIsMappedAroundCodepoints() {
         val state = TextFieldState("a${SingleSurrogateCodepointString}c")
diff --git a/compose/foundation/foundation/src/androidInstrumentedTest/kotlin/androidx/compose/foundation/text2/input/TextFieldCursorTest.kt b/compose/foundation/foundation/src/androidInstrumentedTest/kotlin/androidx/compose/foundation/text2/input/TextFieldCursorTest.kt
index 7ac722a..db12a68 100644
--- a/compose/foundation/foundation/src/androidInstrumentedTest/kotlin/androidx/compose/foundation/text2/input/TextFieldCursorTest.kt
+++ b/compose/foundation/foundation/src/androidInstrumentedTest/kotlin/androidx/compose/foundation/text2/input/TextFieldCursorTest.kt
@@ -873,8 +873,8 @@
             val startEvent = makeTextDragEvent(DragEvent.ACTION_DRAG_STARTED)
             val enterEvent = makeTextDragEvent(DragEvent.ACTION_DRAG_ENTERED)
             val moveEvent = makeTextDragEvent(
-                DragEvent.ACTION_DRAG_LOCATION,
-                Offset(with(rule.density) { fontSize.toPx() * 3 }, 5f)
+                action = DragEvent.ACTION_DRAG_LOCATION,
+                offset = Offset(with(rule.density) { fontSize.toPx() * 3 }, 5f)
             )
 
             view?.dispatchDragEvent(startEvent)
@@ -912,8 +912,8 @@
             val startEvent = makeTextDragEvent(DragEvent.ACTION_DRAG_STARTED)
             val enterEvent = makeTextDragEvent(DragEvent.ACTION_DRAG_ENTERED)
             val moveEvent = makeTextDragEvent(
-                DragEvent.ACTION_DRAG_LOCATION,
-                Offset(with(rule.density) { fontSize.toPx() * 3 }, 5f)
+                action = DragEvent.ACTION_DRAG_LOCATION,
+                offset = Offset(with(rule.density) { fontSize.toPx() * 3 }, 5f)
             )
 
             view?.dispatchDragEvent(startEvent)
@@ -957,8 +957,8 @@
             val startEvent = makeTextDragEvent(DragEvent.ACTION_DRAG_STARTED)
             val enterEvent = makeTextDragEvent(DragEvent.ACTION_DRAG_ENTERED)
             val moveEvent = makeTextDragEvent(
-                DragEvent.ACTION_DRAG_LOCATION,
-                Offset(with(rule.density) { fontSize.toPx() * 3 }, 5f)
+                action = DragEvent.ACTION_DRAG_LOCATION,
+                offset = Offset(with(rule.density) { fontSize.toPx() * 3 }, 5f)
             )
 
             view?.dispatchDragEvent(startEvent)
@@ -973,8 +973,8 @@
             .assertCursor(cursorTopCenterInLtr)
 
         val moveEvent2 = makeTextDragEvent(
-            DragEvent.ACTION_DRAG_LOCATION,
-            Offset(with(rule.density) { fontSize.toPx() * 4 }, 5f)
+            action = DragEvent.ACTION_DRAG_LOCATION,
+            offset = Offset(with(rule.density) { fontSize.toPx() * 4 }, 5f)
         )
         view?.dispatchDragEvent(moveEvent2)
         rule.mainClock.advanceTimeBy(400)
@@ -1009,8 +1009,8 @@
             val startEvent = makeTextDragEvent(DragEvent.ACTION_DRAG_STARTED)
             val enterEvent = makeTextDragEvent(DragEvent.ACTION_DRAG_ENTERED)
             val moveEvent = makeTextDragEvent(
-                DragEvent.ACTION_DRAG_LOCATION,
-                Offset(with(rule.density) { fontSize.toPx() * 3 }, 5f)
+                action = DragEvent.ACTION_DRAG_LOCATION,
+                offset = Offset(with(rule.density) { fontSize.toPx() * 3 }, 5f)
             )
 
             view?.dispatchDragEvent(startEvent)
diff --git a/compose/foundation/foundation/src/androidInstrumentedTest/kotlin/androidx/compose/foundation/text2/input/TextFieldDragAndDropTest.kt b/compose/foundation/foundation/src/androidInstrumentedTest/kotlin/androidx/compose/foundation/text2/input/TextFieldDragAndDropTest.kt
index c32aa761..28276f2 100644
--- a/compose/foundation/foundation/src/androidInstrumentedTest/kotlin/androidx/compose/foundation/text2/input/TextFieldDragAndDropTest.kt
+++ b/compose/foundation/foundation/src/androidInstrumentedTest/kotlin/androidx/compose/foundation/text2/input/TextFieldDragAndDropTest.kt
@@ -20,6 +20,7 @@
 import android.view.View
 import androidx.compose.foundation.ExperimentalFoundationApi
 import androidx.compose.foundation.content.DragAndDropScope
+import androidx.compose.foundation.content.createClipData
 import androidx.compose.foundation.content.testDragAndDrop
 import androidx.compose.foundation.interaction.MutableInteractionSource
 import androidx.compose.foundation.interaction.collectIsHoveredAsState
@@ -184,7 +185,10 @@
         rule.setContentAndTestDragAndDrop("aaaa") {
             drag(
                 Offset(fontSize.toPx() * 2, 10f),
-                listOf("Hello", "World")
+                createClipData {
+                    addText("Hello")
+                    addText("World")
+                }
             )
             drop()
             assertThat(state.text.toString()).isEqualTo("aaHello\nWorldaa")
diff --git a/compose/foundation/foundation/src/androidInstrumentedTest/kotlin/androidx/compose/foundation/text2/input/internal/DragAndDropTestUtils.kt b/compose/foundation/foundation/src/androidInstrumentedTest/kotlin/androidx/compose/foundation/text2/input/internal/DragAndDropTestUtils.kt
index af4f35f..c5494a8 100644
--- a/compose/foundation/foundation/src/androidInstrumentedTest/kotlin/androidx/compose/foundation/text2/input/internal/DragAndDropTestUtils.kt
+++ b/compose/foundation/foundation/src/androidInstrumentedTest/kotlin/androidx/compose/foundation/text2/input/internal/DragAndDropTestUtils.kt
@@ -17,11 +17,11 @@
 package androidx.compose.foundation.text2.input.internal
 
 import android.content.ClipData
-import android.content.ClipDescription
 import android.net.Uri
 import android.os.Build
 import android.os.Parcel
 import android.view.DragEvent
+import androidx.compose.foundation.content.createClipData
 import androidx.compose.ui.geometry.Offset
 
 /**
@@ -31,35 +31,22 @@
  * Also it does not mock but uses Parcel to create a DragEvent.
  */
 object DragAndDropTestUtils {
-    private const val LABEL = "Label"
-    const val SAMPLE_TEXT = "Drag Text"
-    val SAMPLE_URI = Uri.parse("http://www.google.com")
+    private const val SAMPLE_TEXT = "Drag Text"
+    private val SAMPLE_URI = Uri.parse("http://www.google.com")
 
     /**
      * Makes a stub drag event containing fake text data.
      *
      * @param action One of the [DragEvent] actions.
      */
-    fun makeTextDragEvent(action: Int, offset: Offset = Offset.Zero): DragEvent {
+    fun makeTextDragEvent(
+        action: Int,
+        text: String = SAMPLE_TEXT,
+        offset: Offset = Offset.Zero,
+    ): DragEvent {
         return makeDragEvent(
             action = action,
-            items = listOf(ClipData.Item(SAMPLE_TEXT)),
-            mimeTypes = listOf(ClipDescription.MIMETYPE_TEXT_PLAIN),
-            offset = offset
-        )
-    }
-
-    /**
-     * Makes a stub drag event containing text data.
-     *
-     * @param action One of the [DragEvent] actions.
-     * @param text The text being dragged.
-     */
-    fun makeTextDragEvent(action: Int, text: String?, offset: Offset = Offset.Zero): DragEvent {
-        return makeDragEvent(
-            action = action,
-            items = listOf(ClipData.Item(text)),
-            mimeTypes = listOf(ClipDescription.MIMETYPE_TEXT_PLAIN),
+            clipData = createClipData { addText(text) },
             offset = offset
         )
     }
@@ -72,29 +59,23 @@
     fun makeImageDragEvent(
         action: Int,
         item: Uri = SAMPLE_URI,
-        offset: Offset = Offset.Zero
+        offset: Offset = Offset.Zero,
     ): DragEvent {
-        // We're not actually resolving Uris in these tests, so this can be anything:
-        val mimeType = "image/*"
         return makeDragEvent(
             action = action,
-            items = listOf(ClipData.Item(item)),
-            mimeTypes = listOf(mimeType),
+            clipData = createClipData {
+                // We're not actually resolving Uris in these tests, so this can be anything:
+                addUri(item, mimeType = "image/png")
+            },
             offset = offset
         )
     }
 
     fun makeDragEvent(
         action: Int,
-        items: List<ClipData.Item>,
-        mimeTypes: List<String>,
+        clipData: ClipData,
         offset: Offset = Offset.Zero
     ): DragEvent {
-        val clipDescription = ClipDescription(LABEL, mimeTypes.toTypedArray())
-        val clipData = ClipData(clipDescription, items.first()).apply {
-            items.drop(1).forEach { addItem(it) }
-        }
-
         val parcel = Parcel.obtain()
 
         parcel.writeInt(action)
@@ -109,7 +90,7 @@
         parcel.writeInt(1)
         clipData.writeToParcel(parcel, 0)
         parcel.writeInt(1)
-        clipDescription.writeToParcel(parcel, 0)
+        clipData.description.writeToParcel(parcel, 0)
 
         parcel.setDataPosition(0)
         return DragEvent.CREATOR.createFromParcel(parcel)
diff --git a/compose/foundation/foundation/src/androidInstrumentedTest/kotlin/androidx/compose/foundation/text2/input/internal/StatelessInputConnectionTest.kt b/compose/foundation/foundation/src/androidInstrumentedTest/kotlin/androidx/compose/foundation/text2/input/internal/StatelessInputConnectionTest.kt
index b10aab3..1653ef2 100644
--- a/compose/foundation/foundation/src/androidInstrumentedTest/kotlin/androidx/compose/foundation/text2/input/internal/StatelessInputConnectionTest.kt
+++ b/compose/foundation/foundation/src/androidInstrumentedTest/kotlin/androidx/compose/foundation/text2/input/internal/StatelessInputConnectionTest.kt
@@ -62,8 +62,11 @@
             this@StatelessInputConnectionTest.onImeAction?.invoke(imeAction)
         }
 
-        override fun requestEdit(block: EditingBuffer.() -> Unit) {
-            onRequestEdit?.invoke(block)
+        override fun requestEdit(
+            notifyImeOfChanges: Boolean,
+            block: EditingBuffer.() -> Unit
+        ) {
+            onRequestEdit?.invoke(notifyImeOfChanges, block)
         }
 
         override fun sendKeyEvent(keyEvent: KeyEvent) {
@@ -85,7 +88,7 @@
             field = value
             state = TextFieldState(value.toString(), value.selectionInChars)
         }
-    private var onRequestEdit: ((EditingBuffer.() -> Unit) -> Unit)? = null
+    private var onRequestEdit: ((Boolean, EditingBuffer.() -> Unit) -> Unit)? = null
     private var onSendKeyEvent: ((KeyEvent) -> Unit)? = null
     private var onImeAction: ((ImeAction) -> Unit)? = null
     private var onCommitContent: ((TransferableContent) -> Boolean)? = null
@@ -188,9 +191,9 @@
     @Test
     fun commitTextTest_batchSession() {
         var requestEditsCalled = 0
-        onRequestEdit = {
+        onRequestEdit = { _, block ->
             requestEditsCalled++
-            state.mainBuffer.it()
+            state.mainBuffer.block()
         }
         value = TextFieldCharSequence(text = "", selection = TextRange.Zero)
 
@@ -320,9 +323,9 @@
     @Test
     fun mixedAPICalls_batchSession() {
         var requestEditsCalled = 0
-        onRequestEdit = {
+        onRequestEdit = { _, block ->
             requestEditsCalled++
-            state.mainBuffer.it()
+            state.mainBuffer.block()
         }
         value = TextFieldCharSequence(text = "", selection = TextRange.Zero)
 
@@ -361,7 +364,7 @@
     @Test
     fun do_not_callback_if_only_readonly_ops() {
         var requestEditsCalled = 0
-        onRequestEdit = { requestEditsCalled++ }
+        onRequestEdit = { _, _ -> requestEditsCalled++ }
         ic.beginBatchEdit()
         ic.getSelectedText(1)
         ic.endBatchEdit()
@@ -414,6 +417,66 @@
     }
 
     @Test
+    fun selectAll_contextMenuAction_triggersSelectionAndImeNotification() {
+        value = TextFieldCharSequence("Hello")
+        var callCount = 0
+        var isNotifyIme = false
+        onRequestEdit = { notify, block ->
+            isNotifyIme = notify
+            callCount++
+            state.mainBuffer.block()
+        }
+
+        ic.performContextMenuAction(android.R.id.selectAll)
+
+        assertThat(callCount).isEqualTo(1)
+        assertThat(isNotifyIme).isTrue()
+        assertThat(state.mainBuffer.selection).isEqualTo(TextRange(0, 5))
+    }
+
+    @Test
+    fun cut_contextMenuAction_triggersSyntheticKeyEvents() {
+        val keyEvents = mutableListOf<KeyEvent>()
+        onSendKeyEvent = { keyEvents += it }
+
+        ic.performContextMenuAction(android.R.id.cut)
+
+        assertThat(keyEvents.size).isEqualTo(2)
+        assertThat(keyEvents[0].action).isEqualTo(KeyEvent.ACTION_DOWN)
+        assertThat(keyEvents[0].keyCode).isEqualTo(KeyEvent.KEYCODE_CUT)
+        assertThat(keyEvents[1].action).isEqualTo(KeyEvent.ACTION_UP)
+        assertThat(keyEvents[1].keyCode).isEqualTo(KeyEvent.KEYCODE_CUT)
+    }
+
+    @Test
+    fun copy_contextMenuAction_triggersSyntheticKeyEvents() {
+        val keyEvents = mutableListOf<KeyEvent>()
+        onSendKeyEvent = { keyEvents += it }
+
+        ic.performContextMenuAction(android.R.id.copy)
+
+        assertThat(keyEvents.size).isEqualTo(2)
+        assertThat(keyEvents[0].action).isEqualTo(KeyEvent.ACTION_DOWN)
+        assertThat(keyEvents[0].keyCode).isEqualTo(KeyEvent.KEYCODE_COPY)
+        assertThat(keyEvents[1].action).isEqualTo(KeyEvent.ACTION_UP)
+        assertThat(keyEvents[1].keyCode).isEqualTo(KeyEvent.KEYCODE_COPY)
+    }
+
+    @Test
+    fun paste_contextMenuAction_triggersSyntheticKeyEvents() {
+        val keyEvents = mutableListOf<KeyEvent>()
+        onSendKeyEvent = { keyEvents += it }
+
+        ic.performContextMenuAction(android.R.id.paste)
+
+        assertThat(keyEvents.size).isEqualTo(2)
+        assertThat(keyEvents[0].action).isEqualTo(KeyEvent.ACTION_DOWN)
+        assertThat(keyEvents[0].keyCode).isEqualTo(KeyEvent.KEYCODE_PASTE)
+        assertThat(keyEvents[1].action).isEqualTo(KeyEvent.ACTION_UP)
+        assertThat(keyEvents[1].keyCode).isEqualTo(KeyEvent.KEYCODE_PASTE)
+    }
+
+    @Test
     fun debugMode_isDisabled() {
         // run this in presubmit to check that we are not accidentally enabling logs on prod
         assertFalse(
diff --git a/compose/foundation/foundation/src/androidInstrumentedTest/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
index c223a84..00c788f 100644
--- a/compose/foundation/foundation/src/androidInstrumentedTest/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
@@ -166,8 +166,8 @@
             val startEvent = makeTextDragEvent(DragEvent.ACTION_DRAG_STARTED)
             val enterEvent = makeTextDragEvent(DragEvent.ACTION_DRAG_ENTERED)
             val moveEvent = makeTextDragEvent(
-                DragEvent.ACTION_DRAG_LOCATION,
-                Offset(40f, 10f)
+                action = DragEvent.ACTION_DRAG_LOCATION,
+                offset = Offset(40f, 10f)
             )
 
             view.dispatchDragEvent(startEvent)
@@ -206,8 +206,8 @@
             val startEvent = makeTextDragEvent(DragEvent.ACTION_DRAG_STARTED)
             val enterEvent = makeTextDragEvent(DragEvent.ACTION_DRAG_ENTERED)
             val moveEvent = makeTextDragEvent(
-                DragEvent.ACTION_DRAG_LOCATION,
-                Offset(40f, 10f)
+                action = DragEvent.ACTION_DRAG_LOCATION,
+                offset = Offset(40f, 10f)
             )
 
             view.dispatchDragEvent(startEvent)
@@ -219,8 +219,8 @@
 
         rule.runOnIdle {
             val moveEvent2 = makeTextDragEvent(
-                DragEvent.ACTION_DRAG_LOCATION,
-                Offset(40f, 40f) // force it out of BTF2's hit box
+                action = DragEvent.ACTION_DRAG_LOCATION,
+                offset = Offset(40f, 40f) // force it out of BTF2's hit box
             )
             view.dispatchDragEvent(moveEvent2)
         }
diff --git a/compose/foundation/foundation/src/androidMain/kotlin/androidx/compose/foundation/lazy/layout/LazyLayoutPrefetcher.android.kt b/compose/foundation/foundation/src/androidMain/kotlin/androidx/compose/foundation/lazy/layout/PrefetchExecutor.android.kt
similarity index 70%
rename from compose/foundation/foundation/src/androidMain/kotlin/androidx/compose/foundation/lazy/layout/LazyLayoutPrefetcher.android.kt
rename to compose/foundation/foundation/src/androidMain/kotlin/androidx/compose/foundation/lazy/layout/PrefetchExecutor.android.kt
index faec6d7..63e7631 100644
--- a/compose/foundation/foundation/src/androidMain/kotlin/androidx/compose/foundation/lazy/layout/LazyLayoutPrefetcher.android.kt
+++ b/compose/foundation/foundation/src/androidMain/kotlin/androidx/compose/foundation/lazy/layout/PrefetchExecutor.android.kt
@@ -24,28 +24,16 @@
 import androidx.compose.runtime.RememberObserver
 import androidx.compose.runtime.collection.mutableVectorOf
 import androidx.compose.runtime.remember
-import androidx.compose.ui.layout.SubcomposeLayoutState
-import androidx.compose.ui.layout.SubcomposeLayoutState.PrecomposedSlotHandle
 import androidx.compose.ui.platform.LocalView
-import androidx.compose.ui.unit.Constraints
 import androidx.compose.ui.util.trace
 import java.util.concurrent.TimeUnit
 
 @ExperimentalFoundationApi
 @Composable
-internal actual fun LazyLayoutPrefetcher(
-    prefetchState: LazyLayoutPrefetchState,
-    itemContentFactory: LazyLayoutItemContentFactory,
-    subcomposeLayoutState: SubcomposeLayoutState
-) {
+internal actual fun rememberDefaultPrefetchExecutor(): PrefetchExecutor {
     val view = LocalView.current
-    remember(subcomposeLayoutState, prefetchState, view) {
-        LazyLayoutPrefetcher(
-            prefetchState,
-            subcomposeLayoutState,
-            itemContentFactory,
-            view
-        )
+    return remember(view) {
+        AndroidPrefetchExecutor(view)
     }
 }
 
@@ -105,21 +93,15 @@
  *    Tracking bug: 187393922
  */
 @ExperimentalFoundationApi
-internal class LazyLayoutPrefetcher(
-    private val prefetchState: LazyLayoutPrefetchState,
-    private val subcomposeLayoutState: SubcomposeLayoutState,
-    private val itemContentFactory: LazyLayoutItemContentFactory,
+internal class AndroidPrefetchExecutor(
     private val view: View
-) : RememberObserver,
-    LazyLayoutPrefetchState.Prefetcher,
-    Runnable,
-    Choreographer.FrameCallback {
+) : PrefetchExecutor, RememberObserver, Runnable, Choreographer.FrameCallback {
 
     /**
      * The list of currently not processed prefetch requests. The requests will be processed one by
      * during subsequent [run]s.
      */
-    private val prefetchRequests = mutableVectorOf<PrefetchRequest>()
+    private val prefetchRequests = mutableVectorOf<PrefetchExecutor.Request>()
 
     /**
      * Average time the prefetching operations takes. Keeping it allows us to not start the work
@@ -158,54 +140,47 @@
         var scheduleForNextFrame = false
         while (prefetchRequests.isNotEmpty() && !scheduleForNextFrame) {
             val request = prefetchRequests[0]
-            val itemProvider = itemContentFactory.itemProvider()
-            if (request.canceled || request.index !in 0 until itemProvider.itemCount) {
+            if (!request.isValid) {
                 prefetchRequests.removeAt(0)
-            } else if (request.precomposeHandle == null) {
-                trace("compose:lazylist:prefetch:compose") {
-                    val beforeTimeNs = System.nanoTime()
-                    // check if there is enough time left in this frame. otherwise, we schedule
-                    // a next frame callback in which we will post the message in the handler again.
-                    if (enoughTimeLeft(beforeTimeNs, nextFrameNs, averagePrecomposeTimeNs) ||
-                        oneOverTimeTaskAllowed
-                    ) {
+            } else if (!request.isComposed) {
+                val beforeTimeNs = System.nanoTime()
+                // check if there is enough time left in this frame. otherwise, we schedule
+                // a next frame callback in which we will post the message in the handler again.
+                if (enoughTimeLeft(
+                        beforeTimeNs,
+                        nextFrameNs,
+                        averagePrecomposeTimeNs
+                    ) || oneOverTimeTaskAllowed
+                ) {
+                    trace("compose:lazylist:prefetch:compose") {
                         oneOverTimeTaskAllowed = false
-                        val key = itemProvider.getKey(request.index)
-                        val contentType = itemProvider.getContentType(request.index)
-                        val content = itemContentFactory.getContent(request.index, key, contentType)
-                        request.precomposeHandle = subcomposeLayoutState.precompose(key, content)
+                        request.performComposition()
                         averagePrecomposeTimeNs = calculateAverageTime(
-                            System.nanoTime() - beforeTimeNs,
-                            averagePrecomposeTimeNs
+                            System.nanoTime() - beforeTimeNs, averagePrecomposeTimeNs
                         )
-                    } else {
-                        scheduleForNextFrame = true
                     }
+                } else {
+                    scheduleForNextFrame = true
                 }
             } else {
-                check(!request.measured) { "request already measured" }
-                trace("compose:lazylist:prefetch:measure") {
-                    val beforeTimeNs = System.nanoTime()
-                    if (enoughTimeLeft(beforeTimeNs, nextFrameNs, averagePremeasureTimeNs) ||
-                        oneOverTimeTaskAllowed
-                    ) {
+                val beforeTimeNs = System.nanoTime()
+                if (enoughTimeLeft(
+                        beforeTimeNs,
+                        nextFrameNs,
+                        averagePremeasureTimeNs
+                    ) || oneOverTimeTaskAllowed
+                ) {
+                    trace("compose:lazylist:prefetch:measure") {
                         oneOverTimeTaskAllowed = false
-                        val handle = request.precomposeHandle!!
-                        repeat(handle.placeablesCount) { placeableIndex ->
-                            handle.premeasure(
-                                placeableIndex,
-                                request.constraints
-                            )
-                        }
+                        request.performMeasure()
                         averagePremeasureTimeNs = calculateAverageTime(
-                            System.nanoTime() - beforeTimeNs,
-                            averagePremeasureTimeNs
+                            System.nanoTime() - beforeTimeNs, averagePremeasureTimeNs
                         )
                         // we finished this request
                         prefetchRequests.removeAt(0)
-                    } else {
-                        scheduleForNextFrame = true
                     }
+                } else {
+                    scheduleForNextFrame = true
                 }
             }
         }
@@ -245,53 +220,27 @@
         }
     }
 
-    override fun schedulePrefetch(
-        index: Int,
-        constraints: Constraints
-    ): LazyLayoutPrefetchState.PrefetchHandle {
-        val request = PrefetchRequest(index, constraints)
+    override fun requestPrefetch(request: PrefetchExecutor.Request) {
         prefetchRequests.add(request)
         if (!prefetchScheduled) {
             prefetchScheduled = true
             // schedule the prefetching
             view.post(this)
         }
-        return request
     }
 
     override fun onRemembered() {
-        prefetchState.prefetcher = this
         isActive = true
     }
 
     override fun onForgotten() {
         isActive = false
-        prefetchState.prefetcher = null
         view.removeCallbacks(this)
         choreographer.removeFrameCallback(this)
     }
 
     override fun onAbandoned() {}
 
-    private class PrefetchRequest(
-        val index: Int,
-        val constraints: Constraints
-    ) : @Suppress("SEALED_INHERITOR_IN_DIFFERENT_MODULE")
-    LazyLayoutPrefetchState.PrefetchHandle {
-
-        var precomposeHandle: PrecomposedSlotHandle? = null
-        var canceled = false
-        var measured = false
-
-        override fun cancel() {
-            if (!canceled) {
-                canceled = true
-                precomposeHandle?.dispose()
-                precomposeHandle = null
-            }
-        }
-    }
-
     companion object {
 
         /**
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 2995f36..4e58bdc 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
@@ -102,9 +102,12 @@
                 override val text: TextFieldCharSequence
                     get() = state.visualText
 
-                override fun requestEdit(block: EditingBuffer.() -> Unit) {
+                override fun requestEdit(
+                    notifyImeOfChanges: Boolean,
+                    block: EditingBuffer.() -> Unit
+                ) {
                     state.editUntransformedTextAsUser(
-                        notifyImeOfChanges = false,
+                        notifyImeOfChanges = notifyImeOfChanges,
                         block = block
                     )
                 }
diff --git a/compose/foundation/foundation/src/androidMain/kotlin/androidx/compose/foundation/text2/input/internal/StatelessInputConnection.android.kt b/compose/foundation/foundation/src/androidMain/kotlin/androidx/compose/foundation/text2/input/internal/StatelessInputConnection.android.kt
index d81a479..7eef0c9 100644
--- a/compose/foundation/foundation/src/androidMain/kotlin/androidx/compose/foundation/text2/input/internal/StatelessInputConnection.android.kt
+++ b/compose/foundation/foundation/src/androidMain/kotlin/androidx/compose/foundation/text2/input/internal/StatelessInputConnection.android.kt
@@ -358,7 +358,8 @@
         logDebug("performContextMenuAction($id)")
         when (id) {
             android.R.id.selectAll -> {
-                addEditCommandWithBatch {
+                // no need to batch context menu actions.
+                session.requestEdit(notifyImeOfChanges = true) {
                     setSelection(0, text.length)
                 }
             }
diff --git a/compose/foundation/foundation/src/androidMain/kotlin/androidx/compose/foundation/text2/input/internal/TextInputSession.android.kt b/compose/foundation/foundation/src/androidMain/kotlin/androidx/compose/foundation/text2/input/internal/TextInputSession.android.kt
index b5edfe8..45f1dac 100644
--- a/compose/foundation/foundation/src/androidMain/kotlin/androidx/compose/foundation/text2/input/internal/TextInputSession.android.kt
+++ b/compose/foundation/foundation/src/androidMain/kotlin/androidx/compose/foundation/text2/input/internal/TextInputSession.android.kt
@@ -40,8 +40,14 @@
 
     /**
      * Callback to execute for InputConnection to communicate the changes requested by the IME.
+     *
+     * @param notifyImeOfChanges Normally any request coming from IME should not be
+     * back-communicated but [InputConnection.performContextMenuAction] does not behave like a
+     * regular IME command. Its changes must be resent to IME to keep it in sync with
+     * [TextFieldState].
+     * @param block Lambda scoped to an EditingBuffer to apply changes direct onto a buffer.
      */
-    fun requestEdit(block: EditingBuffer.() -> Unit)
+    fun requestEdit(notifyImeOfChanges: Boolean = false, block: EditingBuffer.() -> Unit)
 
     /**
      * Delegates IME requested KeyEvents.
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 ab9adee..59d6088 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
@@ -26,6 +26,7 @@
 import androidx.compose.foundation.interaction.MutableInteractionSource
 import androidx.compose.foundation.internal.JvmDefaultWithCompatibility
 import androidx.compose.runtime.Composable
+import androidx.compose.runtime.Stable
 import androidx.compose.runtime.remember
 import androidx.compose.runtime.rememberUpdatedState
 import androidx.compose.ui.Modifier
@@ -184,45 +185,44 @@
  * @param reverseDirection reverse the direction of the scroll, so top to bottom scroll will
  * behave like bottom to top and left to right will behave like right to left.
  */
+@Stable
 fun Modifier.draggable(
     state: DraggableState,
     orientation: Orientation,
     enabled: Boolean = true,
     interactionSource: MutableInteractionSource? = null,
     startDragImmediately: Boolean = false,
-    onDragStarted: suspend CoroutineScope.(startedPosition: Offset) -> Unit = {},
-    onDragStopped: suspend CoroutineScope.(velocity: Float) -> Unit = {},
+    onDragStarted: suspend CoroutineScope.(startedPosition: Offset) -> Unit = NoOpOnDragStarted,
+    onDragStopped: suspend CoroutineScope.(velocity: Float) -> Unit = NoOpOnDragStopped,
     reverseDirection: Boolean = false
 ): Modifier = this then DraggableElement(
     state = state,
     orientation = orientation,
     enabled = enabled,
     interactionSource = interactionSource,
-    startDragImmediately = { startDragImmediately },
+    startDragImmediately = startDragImmediately,
     onDragStarted = onDragStarted,
-    onDragStopped = { velocity -> onDragStopped(velocity.toFloat(orientation)) },
-    reverseDirection = reverseDirection,
-    canDrag = { true }
+    onDragStopped = onDragStopped,
+    reverseDirection = reverseDirection
 )
 
 internal class DraggableElement(
     private val state: DraggableState,
-    private val canDrag: (PointerInputChange) -> Boolean,
     private val orientation: Orientation,
     private val enabled: Boolean,
     private val interactionSource: MutableInteractionSource?,
-    private val startDragImmediately: () -> Boolean,
+    private val startDragImmediately: Boolean,
     private val onDragStarted: suspend CoroutineScope.(startedPosition: Offset) -> Unit,
-    private val onDragStopped: suspend CoroutineScope.(velocity: Velocity) -> Unit,
+    private val onDragStopped: suspend CoroutineScope.(velocity: Float) -> Unit,
     private val reverseDirection: Boolean
 ) : ModifierNodeElement<DraggableNode>() {
     override fun create(): DraggableNode = DraggableNode(
         state,
-        canDrag,
+        CanDrag,
         orientation,
         enabled,
         interactionSource,
-        startDragImmediately,
+        if (startDragImmediately) StartDragImmediately else DoNotStartDragImmediately,
         onDragStarted,
         onDragStopped,
         reverseDirection
@@ -231,11 +231,11 @@
     override fun update(node: DraggableNode) {
         node.update(
             state,
-            canDrag,
+            CanDrag,
             orientation,
             enabled,
             interactionSource,
-            startDragImmediately,
+            if (startDragImmediately) StartDragImmediately else DoNotStartDragImmediately,
             onDragStarted,
             onDragStopped,
             reverseDirection
@@ -250,7 +250,6 @@
         other as DraggableElement
 
         if (state != other.state) return false
-        if (canDrag != other.canDrag) return false
         if (orientation != other.orientation) return false
         if (enabled != other.enabled) return false
         if (interactionSource != other.interactionSource) return false
@@ -264,7 +263,6 @@
 
     override fun hashCode(): Int {
         var result = state.hashCode()
-        result = 31 * result + canDrag.hashCode()
         result = 31 * result + orientation.hashCode()
         result = 31 * result + enabled.hashCode()
         result = 31 * result + (interactionSource?.hashCode() ?: 0)
@@ -277,7 +275,6 @@
 
     override fun InspectorInfo.inspectableProperties() {
         name = "draggable"
-        properties["canDrag"] = canDrag
         properties["orientation"] = orientation
         properties["enabled"] = enabled
         properties["reverseDirection"] = reverseDirection
@@ -287,6 +284,12 @@
         properties["onDragStopped"] = onDragStopped
         properties["state"] = state
     }
+
+    companion object {
+        val StartDragImmediately = { true }
+        val DoNotStartDragImmediately = { false }
+        val CanDrag: (PointerInputChange) -> Boolean = { true }
+    }
 }
 
 internal class DraggableNode(
@@ -297,7 +300,7 @@
     interactionSource: MutableInteractionSource?,
     startDragImmediately: () -> Boolean,
     private var onDragStarted: suspend CoroutineScope.(startedPosition: Offset) -> Unit,
-    private var onDragStopped: suspend CoroutineScope.(velocity: Velocity) -> Unit,
+    private var onDragStopped: suspend CoroutineScope.(velocity: Float) -> Unit,
     reverseDirection: Boolean
 ) : AbstractDraggableNode(
     canDrag,
@@ -321,7 +324,7 @@
         this@DraggableNode.onDragStarted(this, startedPosition)
 
     override suspend fun CoroutineScope.onDragStopped(velocity: Velocity) =
-        this@DraggableNode.onDragStopped(this, velocity)
+        this@DraggableNode.onDragStopped(this, velocity.toFloat(orientation))
 
     fun update(
         state: DraggableState,
@@ -331,7 +334,7 @@
         interactionSource: MutableInteractionSource?,
         startDragImmediately: () -> Boolean,
         onDragStarted: suspend CoroutineScope.(startedPosition: Offset) -> Unit,
-        onDragStopped: suspend CoroutineScope.(velocity: Velocity) -> Unit,
+        onDragStopped: suspend CoroutineScope.(velocity: Float) -> Unit,
         reverseDirection: Boolean
     ) {
         var resetPointerInputHandling = false
@@ -691,3 +694,6 @@
 
 private fun Velocity.toFloat(orientation: Orientation) =
     if (orientation == Orientation.Vertical) this.y else this.x
+
+private val NoOpOnDragStarted: suspend CoroutineScope.(startedPosition: Offset) -> Unit = {}
+private val NoOpOnDragStopped: suspend CoroutineScope.(velocity: Float) -> Unit = {}
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 47afcb9..da8ca4d 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
@@ -607,7 +607,7 @@
 
     val draggableState = ScrollDraggableState(scrollLogic)
     private val startDragImmediately = { scrollLogic.shouldScrollImmediately() }
-    private val onDragStopped: suspend CoroutineScope.(velocity: Velocity) -> Unit = { velocity ->
+    private val onDragStopped: suspend CoroutineScope.(velocity: Float) -> Unit = { velocity ->
         nestedScrollDispatcher.coroutineScope.launch {
             scrollLogic.onDragStopped(velocity)
         }
@@ -742,6 +742,11 @@
 
     fun Offset.reverseIfNeeded(): Offset = if (reverseDirection) this * -1f else this
 
+    fun Float.toVelocity() = Velocity(
+        x = if (orientation == Horizontal) this else 0f,
+        y = if (orientation == Orientation.Vertical) this else 0f,
+    )
+
     /**
      * @return the amount of scroll that was consumed
      */
@@ -793,11 +798,11 @@
         }
     }
 
-    suspend fun onDragStopped(initialVelocity: Velocity) {
+    suspend fun onDragStopped(initialVelocity: Float) {
         // Self started flinging, set
         registerNestedFling(true)
 
-        val availableVelocity = initialVelocity.singleAxisVelocity()
+        val availableVelocity = initialVelocity.toVelocity()
 
         val performFling: suspend (Velocity) -> Velocity = { velocity ->
             val preConsumedByParent = nestedScrollDispatcher
diff --git a/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/lazy/LazyListPrefetchStrategy.kt b/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/lazy/LazyListPrefetchStrategy.kt
new file mode 100644
index 0000000..cc4614b
--- /dev/null
+++ b/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/lazy/LazyListPrefetchStrategy.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.
+ */
+
+package androidx.compose.foundation.lazy
+
+import androidx.compose.foundation.ExperimentalFoundationApi
+import androidx.compose.foundation.lazy.layout.LazyLayoutPrefetchState
+import androidx.compose.foundation.lazy.layout.PrefetchExecutor
+import androidx.compose.runtime.Stable
+
+/**
+ * Implementations of this interface control which indices of a LazyList should be prefetched
+ * (precomposed and premeasured during idle time) as the user interacts with it.
+ *
+ * Implementations should invoke [LazyListPrefetchScope.schedulePrefetch] to schedule prefetches
+ * from the [onScroll] and [onVisibleItemsUpdated] callbacks. If any of the returned PrefetchHandles
+ * no longer need to be prefetched, use [LazyLayoutPrefetchState.PrefetchHandle.cancel] to cancel
+ * the request.
+ */
+@ExperimentalFoundationApi
+interface LazyListPrefetchStrategy {
+
+    /**
+     * A PrefetchExecutor implementation which will be used to execute prefetch requests for this
+     * strategy implementation. If null, the default PrefetchExecutor for the platform will be used.
+     */
+    val prefetchExecutor: PrefetchExecutor?
+        get() = null
+
+    /**
+     * onScroll is invoked when the LazyList scrolls, whether or not the visible items have changed.
+     * If the visible items have also changed, then this will be invoked in the same frame *after*
+     * [onVisibleItemsUpdated].
+     *
+     * [delta] can be used to understand scroll direction: delta < 0 indicates scrolling down while
+     * delta > 0 indicates scrolling up.
+     */
+    fun LazyListPrefetchScope.onScroll(delta: Float, layoutInfo: LazyListLayoutInfo)
+
+    /**
+     * onVisibleItemsUpdated is invoked when the LazyList scrolls if the visible items have changed.
+     * Info about these visible items can be found in [layoutInfo]'s
+     * [LazyListLayoutInfo.visibleItemsInfo].
+     */
+    fun LazyListPrefetchScope.onVisibleItemsUpdated(layoutInfo: LazyListLayoutInfo)
+}
+
+/**
+ * Scope for callbacks in [LazyListPrefetchStrategy] which allows prefetches to be requested.
+ */
+@ExperimentalFoundationApi
+interface LazyListPrefetchScope {
+
+    /**
+     * Schedules a prefetch for the given index. Requests are executed in the order they're
+     * requested. If a requested prefetch is no longer necessary (for example, due to changing
+     * scroll direction), the request should be canceled via
+     * [LazyLayoutPrefetchState.PrefetchHandle.cancel].
+     *
+     * See [PrefetchExecutor].
+     */
+    fun schedulePrefetch(index: Int): LazyLayoutPrefetchState.PrefetchHandle
+}
+
+/**
+ * The default prefetching strategy for LazyLists - this will be used automatically if no other
+ * strategy is provided.
+ */
+@OptIn(ExperimentalFoundationApi::class)
+@Stable
+internal class DefaultLazyListPrefetchStrategy : LazyListPrefetchStrategy {
+
+    /**
+     * The index scheduled to be prefetched (or the last prefetched index if the prefetch is done).
+     */
+    private var indexToPrefetch = -1
+
+    /**
+     * The handle associated with the current index from [indexToPrefetch].
+     */
+    private var currentPrefetchHandle: LazyLayoutPrefetchState.PrefetchHandle? = null
+
+    /**
+     * Keeps the scrolling direction during the previous calculation in order to be able to
+     * detect the scrolling direction change.
+     */
+    private var wasScrollingForward = false
+
+    override fun LazyListPrefetchScope.onScroll(delta: Float, layoutInfo: LazyListLayoutInfo) {
+        if (layoutInfo.visibleItemsInfo.isNotEmpty()) {
+            val scrollingForward = delta < 0
+            val indexToPrefetch = if (scrollingForward) {
+                layoutInfo.visibleItemsInfo.last().index + 1
+            } else {
+                layoutInfo.visibleItemsInfo.first().index - 1
+            }
+            if (indexToPrefetch != this@DefaultLazyListPrefetchStrategy.indexToPrefetch &&
+                indexToPrefetch in 0 until layoutInfo.totalItemsCount
+            ) {
+                if (wasScrollingForward != scrollingForward) {
+                    // the scrolling direction has been changed which means the last prefetched
+                    // is not going to be reached anytime soon so it is safer to dispose it.
+                    // if this item is already visible it is safe to call the method anyway
+                    // as it will be no-op
+                    currentPrefetchHandle?.cancel()
+                }
+                this@DefaultLazyListPrefetchStrategy.wasScrollingForward = scrollingForward
+                this@DefaultLazyListPrefetchStrategy.indexToPrefetch = indexToPrefetch
+                currentPrefetchHandle = schedulePrefetch(
+                    indexToPrefetch
+                )
+            }
+        }
+    }
+
+    override fun LazyListPrefetchScope.onVisibleItemsUpdated(layoutInfo: LazyListLayoutInfo) {
+        if (indexToPrefetch != -1 && layoutInfo.visibleItemsInfo.isNotEmpty()) {
+            val expectedPrefetchIndex = if (wasScrollingForward) {
+                layoutInfo.visibleItemsInfo.last().index + 1
+            } else {
+                layoutInfo.visibleItemsInfo.first().index - 1
+            }
+            if (indexToPrefetch != expectedPrefetchIndex) {
+                indexToPrefetch = -1
+                currentPrefetchHandle?.cancel()
+                currentPrefetchHandle = null
+            }
+        }
+    }
+}
diff --git a/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/lazy/LazyListState.kt b/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/lazy/LazyListState.kt
index 2090fa7..5528563 100644
--- a/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/lazy/LazyListState.kt
+++ b/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/lazy/LazyListState.kt
@@ -31,6 +31,7 @@
 import androidx.compose.foundation.gestures.ScrollableState
 import androidx.compose.foundation.interaction.InteractionSource
 import androidx.compose.foundation.interaction.MutableInteractionSource
+import androidx.compose.foundation.lazy.LazyListState.Companion.Saver
 import androidx.compose.foundation.lazy.layout.AwaitFirstLayoutModifier
 import androidx.compose.foundation.lazy.layout.LazyLayoutBeyondBoundsInfo
 import androidx.compose.foundation.lazy.layout.LazyLayoutPinnedItemList
@@ -58,7 +59,6 @@
 import kotlin.coroutines.EmptyCoroutineContext
 import kotlin.math.abs
 import kotlin.ranges.IntRange
-import kotlin.ranges.until
 import kotlinx.coroutines.CoroutineScope
 import kotlinx.coroutines.launch
 
@@ -86,6 +86,34 @@
 }
 
 /**
+ * Creates a [LazyListState] that is remembered across compositions.
+ *
+ * Changes to the provided initial values will **not** result in the state being recreated or
+ * changed in any way if it has already been created.
+ *
+ * @param initialFirstVisibleItemIndex the initial value for [LazyListState.firstVisibleItemIndex]
+ * @param initialFirstVisibleItemScrollOffset the initial value for
+ * [LazyListState.firstVisibleItemScrollOffset]
+ * @param prefetchStrategy the [LazyListPrefetchStrategy] to use for prefetching content in this
+ * list
+ */
+@ExperimentalFoundationApi
+@Composable
+fun rememberLazyListState(
+    initialFirstVisibleItemIndex: Int = 0,
+    initialFirstVisibleItemScrollOffset: Int = 0,
+    prefetchStrategy: LazyListPrefetchStrategy = DefaultLazyListPrefetchStrategy(),
+): LazyListState {
+    return rememberSaveable(saver = LazyListState.saver(prefetchStrategy)) {
+        LazyListState(
+            initialFirstVisibleItemIndex,
+            initialFirstVisibleItemScrollOffset,
+            prefetchStrategy
+        )
+    }
+}
+
+/**
  * A state object that can be hoisted to control and observe scrolling.
  *
  * In most cases, this will be created via [rememberLazyListState].
@@ -93,14 +121,27 @@
  * @param firstVisibleItemIndex the initial value for [LazyListState.firstVisibleItemIndex]
  * @param firstVisibleItemScrollOffset the initial value for
  * [LazyListState.firstVisibleItemScrollOffset]
+ * @param prefetchStrategy the [LazyListPrefetchStrategy] to use for prefetching content in this
+ * list
  */
 @OptIn(ExperimentalFoundationApi::class)
 @Stable
-class LazyListState constructor(
+class LazyListState @ExperimentalFoundationApi constructor(
     firstVisibleItemIndex: Int = 0,
-    firstVisibleItemScrollOffset: Int = 0
+    firstVisibleItemScrollOffset: Int = 0,
+    private val prefetchStrategy: LazyListPrefetchStrategy = DefaultLazyListPrefetchStrategy(),
 ) : ScrollableState {
 
+    /**
+     * @param firstVisibleItemIndex the initial value for [LazyListState.firstVisibleItemIndex]
+     * @param firstVisibleItemScrollOffset the initial value for
+     * [LazyListState.firstVisibleItemScrollOffset]
+     */
+    constructor(
+        firstVisibleItemIndex: Int = 0,
+        firstVisibleItemScrollOffset: Int = 0
+    ) : this(firstVisibleItemIndex, firstVisibleItemScrollOffset, DefaultLazyListPrefetchStrategy())
+
     internal var hasLookaheadPassOccurred: Boolean = false
         private set
     internal var postLookaheadLayoutInfo: LazyListMeasureResult? = null
@@ -145,6 +186,7 @@
         EmptyLazyListMeasureResult,
         neverEqualPolicy()
     )
+
     /**
      * The object of [LazyListLayoutInfo] calculated during the last layout pass. For example,
      * you can use it to calculate what items are currently visible.
@@ -198,22 +240,6 @@
     internal var prefetchingEnabled: Boolean = true
 
     /**
-     * The index scheduled to be prefetched (or the last prefetched index if the prefetch is done).
-     */
-    private var indexToPrefetch = -1
-
-    /**
-     * The handle associated with the current index from [indexToPrefetch].
-     */
-    private var currentPrefetchHandle: LazyLayoutPrefetchState.PrefetchHandle? = null
-
-    /**
-     * Keeps the scrolling direction during the previous calculation in order to be able to
-     * detect the scrolling direction change.
-     */
-    private var wasScrollingForward = false
-
-    /**
      * The [Remeasurement] object associated with our layout. It allows us to remeasure
      * synchronously during scroll.
      */
@@ -239,6 +265,19 @@
 
     internal val beyondBoundsInfo = LazyLayoutBeyondBoundsInfo()
 
+    internal val prefetchState = LazyLayoutPrefetchState(prefetchStrategy.prefetchExecutor)
+
+    private val prefetchScope = object : LazyListPrefetchScope {
+        override fun schedulePrefetch(index: Int): LazyLayoutPrefetchState.PrefetchHandle {
+            // Without read observation since this can be triggered from scroll - this will then
+            // cause us to recompose when the measure result changes. We don't care since the
+            // prefetch is best effort.
+            val constraints =
+                Snapshot.withoutReadObservation { layoutInfoState.value.childConstraints }
+            return prefetchState.schedulePrefetch(index, constraints)
+        }
+    }
+
     /**
      * Stores currently pinned items which are always composed.
      */
@@ -340,11 +379,16 @@
                 // we don't need to remeasure, so we only trigger re-placement:
                 placementScopeInvalidator.invalidateScope()
 
-                notifyPrefetch(preScrollToBeConsumed - scrollToBeConsumed, layoutInfo)
+                notifyPrefetchOnScroll(
+                    preScrollToBeConsumed - scrollToBeConsumed,
+                    layoutInfo
+                )
             } else {
                 remeasurement?.forceRemeasure()
-
-                notifyPrefetch(preScrollToBeConsumed - scrollToBeConsumed)
+                notifyPrefetchOnScroll(
+                    preScrollToBeConsumed - scrollToBeConsumed,
+                    this.layoutInfo
+                )
             }
         }
 
@@ -362,57 +406,17 @@
         }
     }
 
-    private fun notifyPrefetch(
-        delta: Float,
-        layoutInfo: LazyListMeasureResult = layoutInfoState.value
-    ) {
-        if (!prefetchingEnabled) {
-            return
-        }
-        val info = layoutInfo
-        if (info.visibleItemsInfo.isNotEmpty()) {
-            val scrollingForward = delta < 0
-            val indexToPrefetch = if (scrollingForward) {
-                info.visibleItemsInfo.last().index + 1
-            } else {
-                info.visibleItemsInfo.first().index - 1
-            }
-            if (indexToPrefetch != this.indexToPrefetch &&
-                indexToPrefetch in 0 until info.totalItemsCount
-            ) {
-                if (wasScrollingForward != scrollingForward) {
-                    // the scrolling direction has been changed which means the last prefetched
-                    // is not going to be reached anytime soon so it is safer to dispose it.
-                    // if this item is already visible it is safe to call the method anyway
-                    // as it will be no-op
-                    currentPrefetchHandle?.cancel()
-                }
-                this.wasScrollingForward = scrollingForward
-                this.indexToPrefetch = indexToPrefetch
-                currentPrefetchHandle = prefetchState.schedulePrefetch(
-                    indexToPrefetch, layoutInfo.childConstraints
+    fun notifyPrefetchOnScroll(delta: Float, layoutInfo: LazyListLayoutInfo) {
+        if (prefetchingEnabled) {
+            with(prefetchStrategy) {
+                prefetchScope.onScroll(
+                    delta,
+                    layoutInfo
                 )
             }
         }
     }
 
-    private fun cancelPrefetchIfVisibleItemsChanged(info: LazyListLayoutInfo) {
-        if (indexToPrefetch != -1 && info.visibleItemsInfo.isNotEmpty()) {
-            val expectedPrefetchIndex = if (wasScrollingForward) {
-                info.visibleItemsInfo.last().index + 1
-            } else {
-                info.visibleItemsInfo.first().index - 1
-            }
-            if (indexToPrefetch != expectedPrefetchIndex) {
-                indexToPrefetch = -1
-                currentPrefetchHandle?.cancel()
-                currentPrefetchHandle = null
-            }
-        }
-    }
-
-    internal val prefetchState = LazyLayoutPrefetchState()
-
     /**
      * Animate (smooth scroll) to the given item.
      *
@@ -449,17 +453,23 @@
             if (isLookingAhead) {
                 hasLookaheadPassOccurred = true
             }
-            if (visibleItemsStayedTheSame) {
-                scrollPosition.updateScrollOffset(result.firstVisibleItemScrollOffset)
-            } else {
-                scrollPosition.updateFromMeasureResult(result)
-                cancelPrefetchIfVisibleItemsChanged(result)
-            }
+
             canScrollBackward = result.canScrollBackward
             canScrollForward = result.canScrollForward
             scrollToBeConsumed -= result.consumedScroll
             layoutInfoState.value = result
 
+            if (visibleItemsStayedTheSame) {
+                scrollPosition.updateScrollOffset(result.firstVisibleItemScrollOffset)
+            } else {
+                scrollPosition.updateFromMeasureResult(result)
+                if (prefetchingEnabled) {
+                    with(prefetchStrategy) {
+                        prefetchScope.onVisibleItemsUpdated(result)
+                    }
+                }
+            }
+
             if (isLookingAhead) {
                 updateScrollDeltaForPostLookahead(
                     result.scrollBackAmount,
@@ -538,6 +548,23 @@
                 )
             }
         )
+
+        /**
+         * A [Saver] implementation for [LazyListState] that handles setting a custom
+         * [LazyListPrefetchStrategy].
+         */
+        @ExperimentalFoundationApi
+        internal fun saver(prefetchStrategy: LazyListPrefetchStrategy): Saver<LazyListState, *> =
+            listSaver(
+                save = { listOf(it.firstVisibleItemIndex, it.firstVisibleItemScrollOffset) },
+                restore = {
+                    LazyListState(
+                        firstVisibleItemIndex = it[0],
+                        firstVisibleItemScrollOffset = it[1],
+                        prefetchStrategy
+                    )
+                }
+            )
     }
 }
 
@@ -551,6 +578,7 @@
     measureResult = object : MeasureResult {
         override val width: Int = 0
         override val height: Int = 0
+
         @Suppress("PrimitiveInCollection")
         override val alignmentLines: Map<AlignmentLine, Int> = emptyMap()
         override fun placeChildren() {}
diff --git a/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/lazy/layout/LazyLayout.kt b/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/lazy/layout/LazyLayout.kt
index 031d0f1..f1d49fd 100644
--- a/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/lazy/layout/LazyLayout.kt
+++ b/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/lazy/layout/LazyLayout.kt
@@ -18,6 +18,7 @@
 
 import androidx.compose.foundation.ExperimentalFoundationApi
 import androidx.compose.runtime.Composable
+import androidx.compose.runtime.DisposableEffect
 import androidx.compose.runtime.remember
 import androidx.compose.runtime.rememberUpdatedState
 import androidx.compose.ui.Modifier
@@ -85,12 +86,23 @@
         val subcomposeLayoutState = remember {
             SubcomposeLayoutState(LazyLayoutItemReusePolicy(itemContentFactory))
         }
-        prefetchState?.let {
-            LazyLayoutPrefetcher(
+        if (prefetchState != null) {
+            val executor = prefetchState.prefetchExecutor ?: rememberDefaultPrefetchExecutor()
+            DisposableEffect(
                 prefetchState,
                 itemContentFactory,
-                subcomposeLayoutState
-            )
+                subcomposeLayoutState,
+                executor
+            ) {
+                prefetchState.prefetchHandleProvider = PrefetchHandleProvider(
+                    itemContentFactory,
+                    subcomposeLayoutState,
+                    executor
+                )
+                onDispose {
+                    prefetchState.prefetchHandleProvider = null
+                }
+            }
         }
 
         SubcomposeLayout(
@@ -143,15 +155,3 @@
  * 5 (RecycledViewPool.DEFAULT_MAX_SCRAP) + 2 (Recycler.DEFAULT_CACHE_SIZE)
  */
 private const val MaxItemsToRetainForReuse = 7
-
-/**
- * Platform specific implementation of lazy layout items prefetching - precomposing next items in
- * advance during the scrolling.
- */
-@ExperimentalFoundationApi
-@Composable
-internal expect fun LazyLayoutPrefetcher(
-    prefetchState: LazyLayoutPrefetchState,
-    itemContentFactory: LazyLayoutItemContentFactory,
-    subcomposeLayoutState: SubcomposeLayoutState
-)
diff --git a/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/lazy/layout/LazyLayoutPrefetchState.kt b/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/lazy/layout/LazyLayoutPrefetchState.kt
index 3aa8757..eac40e5 100644
--- a/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/lazy/layout/LazyLayoutPrefetchState.kt
+++ b/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/lazy/layout/LazyLayoutPrefetchState.kt
@@ -17,7 +17,9 @@
 package androidx.compose.foundation.lazy.layout
 
 import androidx.compose.foundation.ExperimentalFoundationApi
+import androidx.compose.foundation.lazy.layout.LazyLayoutPrefetchState.PrefetchHandle
 import androidx.compose.runtime.Stable
+import androidx.compose.ui.layout.SubcomposeLayoutState
 import androidx.compose.ui.unit.Constraints
 
 /**
@@ -26,11 +28,14 @@
  * Note: this class is a part of [LazyLayout] harness that allows for building custom lazy
  * layouts. LazyLayout and all corresponding APIs are still under development and are subject to
  * change.
+ *
+ * @param prefetchExecutor the PrefetchExecutor implementation to use to execute prefetch requests.
+ * If null is provided, the default PrefetchExecutor for the platform will be used.
  */
 @ExperimentalFoundationApi
 @Stable
-class LazyLayoutPrefetchState {
-    internal var prefetcher: Prefetcher? = null
+class LazyLayoutPrefetchState(internal val prefetchExecutor: PrefetchExecutor? = null) {
+    internal var prefetchHandleProvider: PrefetchHandleProvider? = null
 
     /**
      * Schedules precomposition and premeasure for the new item.
@@ -39,7 +44,7 @@
      * @param constraints [Constraints] to use for premeasuring.
      */
     fun schedulePrefetch(index: Int, constraints: Constraints): PrefetchHandle {
-        return prefetcher?.schedulePrefetch(index, constraints) ?: DummyHandle
+        return prefetchHandleProvider?.schedulePrefetch(index, constraints) ?: DummyHandle
     }
 
     sealed interface PrefetchHandle {
@@ -49,13 +54,84 @@
          */
         fun cancel()
     }
-
-    internal interface Prefetcher {
-        fun schedulePrefetch(index: Int, constraints: Constraints): PrefetchHandle
-    }
 }
 
 @ExperimentalFoundationApi
-private object DummyHandle : LazyLayoutPrefetchState.PrefetchHandle {
+private object DummyHandle : PrefetchHandle {
     override fun cancel() {}
 }
+
+/**
+ * PrefetchHandleProvider is used to connect the [LazyLayoutPrefetchState], which provides the API
+ * to schedule prefetches, to a [LazyLayoutItemContentFactory] which resolves key and content from
+ * an index, [SubcomposeLayoutState] which knows how to precompose/premeasure,
+ * and a specific [PrefetchExecutor] used to execute a request.
+ */
+@ExperimentalFoundationApi
+internal class PrefetchHandleProvider(
+    private val itemContentFactory: LazyLayoutItemContentFactory,
+    private val subcomposeLayoutState: SubcomposeLayoutState,
+    private val executor: PrefetchExecutor
+) {
+    fun schedulePrefetch(index: Int, constraints: Constraints): PrefetchHandle =
+        HandleAndRequestImpl(index, constraints).also {
+            executor.requestPrefetch(it)
+        }
+
+    @ExperimentalFoundationApi
+    private inner class HandleAndRequestImpl(
+        private val index: Int,
+        private val constraints: Constraints
+    ) : PrefetchHandle, PrefetchExecutor.Request {
+
+        private var precomposeHandle: SubcomposeLayoutState.PrecomposedSlotHandle? = null
+        private var isMeasured = false
+        private var isCanceled = false
+
+        override val isValid: Boolean
+            get() = !isCanceled &&
+                index in 0 until itemContentFactory.itemProvider().itemCount
+
+        override val isComposed get() = precomposeHandle != null
+
+        override fun cancel() {
+            if (!isCanceled) {
+                isCanceled = true
+                precomposeHandle?.dispose()
+                precomposeHandle = null
+            }
+        }
+
+        override fun performComposition() {
+            require(isValid) {
+                "Callers should check whether the request is still valid before calling " +
+                    "performComposition()"
+            }
+            require(precomposeHandle == null) { "Request was already composed!" }
+            val itemProvider = itemContentFactory.itemProvider()
+            val key = itemProvider.getKey(index)
+            val contentType = itemProvider.getContentType(index)
+            val content = itemContentFactory.getContent(index, key, contentType)
+            precomposeHandle = subcomposeLayoutState.precompose(key, content)
+        }
+
+        override fun performMeasure() {
+            require(!isCanceled) {
+                "Callers should check whether the request is still valid before calling " +
+                    "performMeasure()"
+            }
+            require(!isMeasured) { "Request was already measured!" }
+            isMeasured = true
+            val handle = requireNotNull(precomposeHandle) {
+                "performComposition() must be called before performMeasure()"
+            }
+            repeat(handle.placeablesCount) { placeableIndex ->
+                handle.premeasure(placeableIndex, constraints)
+            }
+        }
+
+        override fun toString(): String =
+            "HandleAndRequestImpl { index = $index, constraints = $constraints, " +
+                "isComposed = $isComposed, isMeasured = $isMeasured, isCanceled = $isCanceled }"
+    }
+}
diff --git a/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/lazy/layout/PrefetchExecutor.kt b/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/lazy/layout/PrefetchExecutor.kt
new file mode 100644
index 0000000..7e1b31e
--- /dev/null
+++ b/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/lazy/layout/PrefetchExecutor.kt
@@ -0,0 +1,69 @@
+/*
+ * Copyright 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.compose.foundation.lazy.layout
+
+import androidx.compose.foundation.ExperimentalFoundationApi
+import androidx.compose.runtime.Composable
+
+/**
+ * Remembers the platform-specific implementation for scheduling lazy layout item prefetch
+ * (pre-composing next items in advance during the scrolling).
+ */
+@ExperimentalFoundationApi
+@Composable
+internal expect fun rememberDefaultPrefetchExecutor(): PrefetchExecutor
+
+/**
+ * Implementations of this interface accept prefetch requests via [requestPrefetch] and decide when
+ * to execute them in a way that will have minimal impact on user experience, e.g. during frame idle
+ * time. Executing a request involves invoking [Request.performComposition] and
+ * [Request.performMeasure].
+ */
+@ExperimentalFoundationApi
+interface PrefetchExecutor {
+
+    /**
+     * Accepts a prefetch request. Implementations should find a time to execute them which will
+     * have minimal impact on user experience.
+     */
+    fun requestPrefetch(request: Request)
+
+    sealed interface Request {
+
+        /**
+         * Whether this is still a valid request (wasn't canceled, within list bounds). If it's
+         * not valid, it should be dropped and not executed.
+         */
+        val isValid: Boolean
+
+        /**
+         * Whether this request has been composed via [performComposition].
+         */
+        val isComposed: Boolean
+
+        /**
+         * Composes the content belonging to this request.
+         */
+        fun performComposition()
+
+        /**
+         * Measures the Composition belonging to this request. Must be called after
+         * [performComposition].
+         */
+        fun performMeasure()
+    }
+}
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 64b2cfd..dfc0dae 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
@@ -444,6 +444,7 @@
             -firstItemOffsets[it]
         }
 
+        val minVisibleOffset = minOffset + mainAxisSpacing
         val maxOffset = (mainAxisAvailableSize + afterContentPadding).coerceAtLeast(0)
 
         debugLog {
@@ -473,14 +474,20 @@
             )
 
             laneInfo.setLane(itemIndex, spanRange.laneInfo)
-            val offset = currentItemOffsets.maxInRange(spanRange) + measuredItem.sizeWithSpacings
+            val offset = currentItemOffsets.maxInRange(spanRange)
             spanRange.forEach { lane ->
-                currentItemOffsets[lane] = offset
+                currentItemOffsets[lane] = offset + measuredItem.sizeWithSpacings
                 currentItemIndices[lane] = itemIndex
                 measuredItems[lane].addLast(measuredItem)
             }
 
-            if (currentItemOffsets[spanRange.start] <= minOffset + mainAxisSpacing) {
+            // item is not visible if both start and end bounds are outside of the visible range.
+            if (
+                offset < minVisibleOffset && currentItemOffsets[spanRange.start] <= minVisibleOffset
+            ) {
+                // We scrolled past measuredItem, and it is not visible anymore. We measured it
+                // for correct positioning of other items, but there's no need to place it.
+                // Mark it as not visible and filter below.
                 measuredItem.isVisible = false
                 remeasureNeeded = true
             }
@@ -538,7 +545,10 @@
             }
             laneInfo.setGaps(itemIndex, gaps)
 
-            if (currentItemOffsets[spanRange.start] <= minOffset + mainAxisSpacing) {
+            // item is not visible if both start and end bounds are outside of the visible range.
+            if (
+                offset < minVisibleOffset && currentItemOffsets[spanRange.start] <= minVisibleOffset
+            ) {
                 // We scrolled past measuredItem, and it is not visible anymore. We measured it
                 // for correct positioning of other items, but there's no need to place it.
                 // Mark it as not visible and filter below.
@@ -564,6 +574,7 @@
             firstItemIndices[laneIndex] = laneItems.firstOrNull()?.index ?: Unset
         }
 
+        // ensure no spacing for the last item
         if (currentItemIndices.any { it == itemCount - 1 }) {
             currentItemOffsets.offsetBy(-mainAxisSpacing)
         }
@@ -592,13 +603,21 @@
                 // Note that it is different from initial pass up where we selected largest index
                 // instead. The reason is that we already distributed items on downward pass and
                 // gap would be incorrect if those are moved.
-                val laneIndex = firstItemOffsets.indexOfMinValue()
+                var laneIndex = firstItemOffsets.indexOfMinValue()
+                val nextLaneIndex = firstItemIndices.indexOfMaxValue()
 
-                if (laneIndex != firstItemIndices.indexOfMaxValue()) {
-                    // If min offset lane doesn't have largest value, it means items are misaligned.
-                    // The correct thing here is to restart measure. We will measure up to the end
-                    // and restart measure from there after this pass.
-                    gapDetected = true
+                if (laneIndex != nextLaneIndex) {
+                    if (firstItemOffsets[laneIndex] == firstItemOffsets[nextLaneIndex]) {
+                        // If the offsets are the same, it means that there's no gap here.
+                        // In this case, we should choose the lane where item would go normally.
+                        laneIndex = nextLaneIndex
+                    } else {
+                        // If min offset lane doesn't have largest value, it means items are
+                        // misaligned.
+                        // The correct thing here is to restart measure. We will measure up to the
+                        // end and restart measure from there after this pass.
+                        gapDetected = true
+                    }
                 }
 
                 val currentIndex =
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 973ed6d..97add2d 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
@@ -14,6 +14,8 @@
  * limitations under the License.
  */
 
+@file:OptIn(ExperimentalFoundationApi::class)
+
 package androidx.compose.foundation.pager
 
 import androidx.compose.animation.core.AnimationSpec
@@ -63,7 +65,7 @@
 import kotlin.math.roundToInt
 import kotlinx.coroutines.coroutineScope
 
-@ExperimentalFoundationApi
+@OptIn(ExperimentalFoundationApi::class)
 @Composable
 internal fun Pager(
     /** Modifier to be applied for the inner layout */
@@ -267,7 +269,6 @@
 /**
  * A modifier to detect up and down events in a Pager.
  */
-@OptIn(ExperimentalFoundationApi::class)
 private fun Modifier.dragDirectionDetector(state: PagerState) =
     this then Modifier.pointerInput(state) {
         coroutineScope {
@@ -328,7 +329,6 @@
 /**
  * Wraps [SnapFlingBehavior] to give out information about target page coming from flings.
  */
-@OptIn(ExperimentalFoundationApi::class)
 private class PagerWrapperFlingBehavior(
     val originalFlingBehavior: TargetedFlingBehavior,
     val pagerState: PagerState
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 13b10ea..781762c 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
@@ -19,10 +19,8 @@
 import androidx.annotation.FloatRange
 import androidx.compose.animation.core.AnimationSpec
 import androidx.compose.animation.core.DecayAnimationSpec
-import androidx.compose.animation.core.LinearEasing
 import androidx.compose.animation.core.Spring
 import androidx.compose.animation.core.spring
-import androidx.compose.animation.core.tween
 import androidx.compose.animation.rememberSplineBasedDecay
 import androidx.compose.foundation.ExperimentalFoundationApi
 import androidx.compose.foundation.gestures.FlingBehavior
@@ -105,7 +103,6 @@
  * @param pageContent This Pager's page Composable.
  */
 @Composable
-@ExperimentalFoundationApi
 fun HorizontalPager(
     state: PagerState,
     modifier: Modifier = Modifier,
@@ -192,7 +189,6 @@
  * @param pageContent This Pager's page Composable.
  */
 @Composable
-@ExperimentalFoundationApi
 fun VerticalPager(
     state: PagerState,
     modifier: Modifier = Modifier,
@@ -234,7 +230,6 @@
 /**
  * Contains the default values used by [Pager].
  */
-@ExperimentalFoundationApi
 object PagerDefaults {
 
     /**
@@ -291,6 +286,7 @@
      * position. If the velocity is high enough, the Pager will use the logic described in
      * [decayAnimationSpec] and [snapAnimationSpec].
      */
+    @OptIn(ExperimentalFoundationApi::class)
     @Composable
     fun flingBehavior(
         state: PagerState,
@@ -335,89 +331,6 @@
     }
 
     /**
-     * A [SnapFlingBehavior] that will snap pages to the start of the layout. One can use the
-     * given parameters to control how the snapping animation will happen.
-     * @see androidx.compose.foundation.gestures.snapping.SnapFlingBehavior for more information
-     * on what which parameter controls in the overall snapping animation.
-     *
-     * The animation specs used by the fling behavior will depend on 2 factors:
-     * 1) The gesture velocity.
-     * 2) The target page proposed by [pagerSnapDistance].
-     *
-     * If you're using single page snapping (the most common use case for [Pager]), there won't
-     * be enough space to actually run a decay animation to approach the target page, so the Pager
-     * will always use the snapping animation from [snapAnimationSpec].
-     * If you're using multi-page snapping (this means you're abs(targetPage - currentPage) > 1)
-     * the Pager may use [highVelocityAnimationSpec] or [lowVelocityAnimationSpec] to approach the
-     * targetPage, it will depend on the velocity generated by the triggering gesture.
-     * If the gesture has a high enough velocity to approach the target page, the Pager will use
-     * [highVelocityAnimationSpec] followed by [snapAnimationSpec] for the final step of the
-     * animation. If the gesture doesn't have enough velocity, the Pager will use
-     * [lowVelocityAnimationSpec] + [snapAnimationSpec] in a similar fashion.
-     *
-     * @param state The [PagerState] that controls the which to which this FlingBehavior will
-     * be applied to.
-     * @param pagerSnapDistance A way to control the snapping destination for this [Pager].
-     * The default behavior will result in any fling going to the next page in the direction of the
-     * fling (if the fling has enough velocity, otherwise  the Pager will bounce back). Use
-     * [PagerSnapDistance.atMost] to define a maximum number of pages this [Pager] is allowed to
-     * fling after scrolling is finished and fling has started.
-     * @param lowVelocityAnimationSpec An animation spec used to approach the target offset. When
-     * the fling velocity is not large enough. Large enough means large enough to naturally decay.
-     * When snapping through many pages, the Pager may not be able to run a decay animation, so it
-     * will use this spec to run an animation to approach the target page requested by
-     * [pagerSnapDistance].
-     * @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. For
-     * single page snapping this usually never happens since there won't be enough space to run a
-     * decay animation.
-     * @param snapAnimationSpec The animation spec used to finally snap to the position. This
-     * animation will be often used in 2 cases: 1) There was enough space to an approach animation,
-     * the Pager will use [snapAnimationSpec] in the last step of the animation to settle the page
-     * into position. 2) There was not enough space to run the approach animation.
-     * @param snapPositionalThreshold If the fling has a low velocity (e.g. slow scroll),
-     * this fling behavior will use this snap threshold in order to determine if the pager should
-     * snap back or move forward. Use a number between 0 and 1 as a fraction of the page size that
-     * needs to be scrolled before the Pager considers it should move to the next page.
-     * For instance, if snapPositionalThreshold = 0.35, it means if this pager is scrolled with a
-     * slow velocity and the Pager scrolls more than 35% of the page size, then will jump to the
-     * next page, if not it scrolls back.
-     * Note that any fling that has high enough velocity will *always* move to the next page
-     * in the direction of the fling.
-     *
-     * @return An instance of [FlingBehavior] that will perform Snapping to the next page by
-     * default. The animation will be governed by the post scroll velocity and the Pager will use
-     * either
-     * [lowVelocityAnimationSpec] or [highVelocityAnimationSpec] to approach the snapped position
-     * If a velocity is not high enough the pager will use [snapAnimationSpec] to reach the snapped
-     * position. If the velocity is high enough, the Pager will use the logic described in
-     * [highVelocityAnimationSpec] and [lowVelocityAnimationSpec].
-     */
-    @Suppress("UNUSED_PARAMETER")
-    @Deprecated(
-        "Please use the overload without lowVelocityAnimationSpec.",
-        level = DeprecationLevel.ERROR
-    )
-    @Composable
-    fun flingBehavior(
-        state: PagerState,
-        pagerSnapDistance: PagerSnapDistance = PagerSnapDistance.atMost(1),
-        lowVelocityAnimationSpec: AnimationSpec<Float> = tween(
-            easing = LinearEasing,
-            durationMillis = LowVelocityAnimationDefaultDuration
-        ),
-        highVelocityAnimationSpec: DecayAnimationSpec<Float> = rememberSplineBasedDecay(),
-        snapAnimationSpec: AnimationSpec<Float> = spring(stiffness = Spring.StiffnessMediumLow),
-        snapPositionalThreshold: Float = 0.5f
-    ) = flingBehavior(
-        state,
-        pagerSnapDistance,
-        highVelocityAnimationSpec,
-        snapAnimationSpec,
-        snapPositionalThreshold
-    )
-
-    /**
      * The default implementation of Pager's pageNestedScrollConnection.
      *
      * @param state state of the pager
@@ -461,7 +374,6 @@
     return (snapOffset - currentPageOffsetFraction * (pageSize + spaceBetweenPages)).roundToInt()
 }
 
-@OptIn(ExperimentalFoundationApi::class)
 private class DefaultPagerNestedScrollConnection(
     val state: PagerState,
     val orientation: Orientation
@@ -534,7 +446,6 @@
     }
 }
 
-@OptIn(ExperimentalFoundationApi::class)
 @Suppress("ComposableModifierFactory")
 @Composable
 internal fun Modifier.pagerSemantics(state: PagerState, isVertical: Boolean): Modifier {
@@ -572,8 +483,6 @@
     })
 }
 
-private const val LowVelocityAnimationDefaultDuration = 500
-
 private inline fun debugLog(generateMsg: () -> String) {
     if (PagerDebugConfig.MainPagerComposable) {
         println("Pager: ${generateMsg()}")
diff --git a/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/pager/PagerLayoutInfo.kt b/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/pager/PagerLayoutInfo.kt
index 3462881..cf6a1da 100644
--- a/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/pager/PagerLayoutInfo.kt
+++ b/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/pager/PagerLayoutInfo.kt
@@ -16,7 +16,6 @@
 
 package androidx.compose.foundation.pager
 
-import androidx.compose.foundation.ExperimentalFoundationApi
 import androidx.compose.foundation.gestures.Orientation
 import androidx.compose.foundation.gestures.snapping.SnapPosition
 import androidx.compose.ui.unit.IntSize
@@ -27,7 +26,6 @@
  *
  * Use [PagerState.layoutInfo] to retrieve this
  */
-@ExperimentalFoundationApi
 sealed interface PagerLayoutInfo {
     /**
      * A list of all pages that are currently visible in the [Pager]
@@ -106,6 +104,5 @@
     val snapPosition: SnapPosition
 }
 
-@ExperimentalFoundationApi
 internal val PagerLayoutInfo.mainAxisViewportSize: Int
     get() = if (orientation == Orientation.Vertical) viewportSize.height else viewportSize.width
diff --git a/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/pager/PagerMeasureResult.kt b/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/pager/PagerMeasureResult.kt
index 1495575..bf6ab3b 100644
--- a/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/pager/PagerMeasureResult.kt
+++ b/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/pager/PagerMeasureResult.kt
@@ -16,14 +16,12 @@
 
 package androidx.compose.foundation.pager
 
-import androidx.compose.foundation.ExperimentalFoundationApi
 import androidx.compose.foundation.gestures.Orientation
 import androidx.compose.foundation.gestures.snapping.SnapPosition
 import androidx.compose.ui.layout.MeasureResult
 import androidx.compose.ui.unit.IntSize
 import androidx.compose.ui.util.fastForEach
 
-@OptIn(ExperimentalFoundationApi::class)
 internal class PagerMeasureResult(
     override val visiblePagesInfo: List<MeasuredPage>,
     override val pageSize: Int,
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 940f9d0..6912ae3 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
@@ -72,7 +72,6 @@
  * snapped position.
  * @param pageCount The amount of pages this Pager will have.
  */
-@ExperimentalFoundationApi
 @Composable
 fun rememberPagerState(
     initialPage: Int = 0,
@@ -102,14 +101,12 @@
  * snapped position.
  * @param pageCount The amount of pages this Pager will have.
  */
-@ExperimentalFoundationApi
 fun PagerState(
     currentPage: Int = 0,
     @FloatRange(from = -0.5, to = 0.5) currentPageOffsetFraction: Float = 0f,
     pageCount: () -> Int
 ): PagerState = DefaultPagerState(currentPage, currentPageOffsetFraction, pageCount)
 
-@ExperimentalFoundationApi
 private class DefaultPagerState(
     currentPage: Int,
     currentPageOffsetFraction: Float,
@@ -148,7 +145,7 @@
  * @param currentPageOffsetFraction The offset of the initial page with respect to the start of
  * the layout.
  */
-@ExperimentalFoundationApi
+@OptIn(ExperimentalFoundationApi::class)
 @Stable
 abstract class PagerState(
     currentPage: Int = 0,
@@ -505,6 +502,7 @@
      * @param pageOffsetFraction A fraction of the page size that indicates the offset the
      * destination page will be offset from its snapped position.
      */
+    @ExperimentalFoundationApi
     fun ScrollScope.updateCurrentPage(
         page: Int,
         @FloatRange(from = -0.5, to = 0.5) pageOffsetFraction: Float = 0.0f
@@ -525,6 +523,7 @@
      * Please refer to the sample to learn how to use this API.
      * @sample androidx.compose.foundation.samples.PagerCustomAnimateScrollToPage
      */
+    @ExperimentalFoundationApi
     fun ScrollScope.updateTargetPage(targetPage: Int) {
         programmaticScrollTargetPage = targetPage.coerceInPageRange()
     }
diff --git a/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/text/TextLinkScope.kt b/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/text/TextLinkScope.kt
index 631e0e1..26d0e5b 100644
--- a/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/text/TextLinkScope.kt
+++ b/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/text/TextLinkScope.kt
@@ -37,15 +37,19 @@
 import androidx.compose.ui.layout.ParentDataModifier
 import androidx.compose.ui.platform.LocalUriHandler
 import androidx.compose.ui.platform.UriHandler
+import androidx.compose.ui.semantics.CustomAccessibilityAction
+import androidx.compose.ui.semantics.customActions
+import androidx.compose.ui.semantics.semantics
+import androidx.compose.ui.semantics.textSelectionRange
 import androidx.compose.ui.text.AnnotatedString
 import androidx.compose.ui.text.LinkAnnotation
 import androidx.compose.ui.text.TextLayoutResult
+import androidx.compose.ui.text.TextRange
 import androidx.compose.ui.unit.Density
 import androidx.compose.ui.unit.IntOffset
 import androidx.compose.ui.unit.LayoutDirection
 import androidx.compose.ui.unit.roundToIntRect
 import androidx.compose.ui.util.fastForEach
-import kotlin.math.min
 
 internal typealias LinkRange = AnnotatedString.Range<LinkAnnotation>
 
@@ -100,19 +104,25 @@
                 val path = it.getPathForRange(range.start, range.end)
 
                 val firstCharBoundingBox = it.getBoundingBox(range.start)
-                val minTop = firstCharBoundingBox.top
-                var minLeft = firstCharBoundingBox.left
-                val firstLine = it.getLineForOffset(range.start)
-                val lastLine = it.getLineForOffset(range.end)
-                // might be enough to just check if the second line exist
-                // if yes - take it's left bound or even just 0
-                // TODO(soboleva) check in RTL
-                for (line in firstLine + 1..lastLine) {
-                    val lineLeft = it.getLineLeft(line)
-                    minLeft = min(minLeft, lineLeft)
+                val lastCharBoundingBox = it.getBoundingBox(range.end - 1)
+
+                val rangeStartLine = it.getLineForOffset(range.start)
+                val rangeEndLine = it.getLineForOffset(range.end)
+
+                val xOffset = if (rangeStartLine == rangeEndLine) {
+                    // if the link occupies a single line, we take the left most position of the
+                    // link's range
+                    minOf(lastCharBoundingBox.left, firstCharBoundingBox.left)
+                } else {
+                    // if the link occupies more than one line, the left sides of the link node and
+                    // text node match so we don't need to do anything
+                    0f
                 }
 
-                path.translate(-Offset(minLeft, minTop))
+                // the top of the top-most (first) character
+                val yOffset = firstCharBoundingBox.top
+
+                path.translate(-Offset(xOffset, yOffset))
                 return path
             }
         }
@@ -136,6 +146,19 @@
             Box(
                 clipModifier
                     .textRange(range.start, range.end)
+                    .semantics {
+                        linkClickHandler?.let {
+                            customActions = listOf(
+                                // this action will be passed down to the Talkback through the
+                                // ClickableSpan's onClick method
+                                CustomAccessibilityAction("") {
+                                    it.onClick(range.item)
+                                    true
+                                }
+                            )
+                            textSelectionRange = TextRange(range.start, range.end)
+                        }
+                    }
                     .pointerHoverIcon(PointerIcon.Hand)
                     .combinedClickable(null, indication, onClick = {
                         handleLink(range.item, uriHandler, linkClickHandler)
diff --git a/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/text/modifiers/TextAnnotatedStringNode.kt b/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/text/modifiers/TextAnnotatedStringNode.kt
index 9766ec8..69d0f74 100644
--- a/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/text/modifiers/TextAnnotatedStringNode.kt
+++ b/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/text/modifiers/TextAnnotatedStringNode.kt
@@ -378,6 +378,9 @@
         getTextLayoutResult(action = localSemanticsTextLayoutResult)
     }
 
+    override val shouldClearDescendantSemantics: Boolean
+        get() = true
+
     fun measureNonExtension(
         measureScope: MeasureScope,
         measurable: Measurable,
diff --git a/compose/foundation/foundation/src/desktopMain/kotlin/androidx/compose/foundation/lazy/layout/LazyLayoutPrefetcher.desktop.kt b/compose/foundation/foundation/src/desktopMain/kotlin/androidx/compose/foundation/lazy/layout/PrefetchExecutor.desktop.kt
similarity index 72%
rename from compose/foundation/foundation/src/desktopMain/kotlin/androidx/compose/foundation/lazy/layout/LazyLayoutPrefetcher.desktop.kt
rename to compose/foundation/foundation/src/desktopMain/kotlin/androidx/compose/foundation/lazy/layout/PrefetchExecutor.desktop.kt
index 40510e7..6f9cc99 100644
--- a/compose/foundation/foundation/src/desktopMain/kotlin/androidx/compose/foundation/lazy/layout/LazyLayoutPrefetcher.desktop.kt
+++ b/compose/foundation/foundation/src/desktopMain/kotlin/androidx/compose/foundation/lazy/layout/PrefetchExecutor.desktop.kt
@@ -18,14 +18,15 @@
 
 import androidx.compose.foundation.ExperimentalFoundationApi
 import androidx.compose.runtime.Composable
-import androidx.compose.ui.layout.SubcomposeLayoutState
 
 @ExperimentalFoundationApi
 @Composable
-internal actual fun LazyLayoutPrefetcher(
-    prefetchState: LazyLayoutPrefetchState,
-    itemContentFactory: LazyLayoutItemContentFactory,
-    subcomposeLayoutState: SubcomposeLayoutState
-) {
-    // there is no prefetch implementation on desktop yet
+actual fun rememberDefaultPrefetchExecutor(): PrefetchExecutor {
+    return NoOpPrefetchExecutor
+}
+
+@ExperimentalFoundationApi
+private object NoOpPrefetchExecutor : PrefetchExecutor {
+    override fun requestPrefetch(request: PrefetchExecutor.Request) {
+    }
 }
diff --git a/compose/integration-tests/macrobenchmark-target/src/main/AndroidManifest.xml b/compose/integration-tests/macrobenchmark-target/src/main/AndroidManifest.xml
index d56e2a2..1873374 100644
--- a/compose/integration-tests/macrobenchmark-target/src/main/AndroidManifest.xml
+++ b/compose/integration-tests/macrobenchmark-target/src/main/AndroidManifest.xml
@@ -91,6 +91,19 @@
             </intent-filter>
         </activity>
         <activity
+            android:name=".FrameExperimentActivity"
+            android:label="FrameExp"
+            android:exported="true">
+            <intent-filter>
+                <action android:name="android.intent.action.MAIN" />
+                <category android:name="android.intent.category.LAUNCHER" />
+            </intent-filter>
+            <intent-filter>
+                <action android:name="androidx.compose.integration.macrobenchmark.target.FRAME_EXPERIMENT_ACTIVITY" />
+                <category android:name="android.intent.category.DEFAULT" />
+            </intent-filter>
+        </activity>
+        <activity
             android:name=".BaselineProfileActivity"
             android:exported="true">
             <intent-filter>
diff --git a/compose/integration-tests/macrobenchmark-target/src/main/java/androidx/compose/integration/macrobenchmark/target/FrameExperimentActivity.kt b/compose/integration-tests/macrobenchmark-target/src/main/java/androidx/compose/integration/macrobenchmark/target/FrameExperimentActivity.kt
new file mode 100644
index 0000000..7c4f269
--- /dev/null
+++ b/compose/integration-tests/macrobenchmark-target/src/main/java/androidx/compose/integration/macrobenchmark/target/FrameExperimentActivity.kt
@@ -0,0 +1,108 @@
+/*
+ * Copyright 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.compose.integration.macrobenchmark.target
+
+import android.content.Context
+import android.graphics.Canvas
+import android.graphics.Color
+import android.graphics.Paint
+import android.os.Bundle
+import android.os.Trace
+import android.view.View
+import androidx.activity.ComponentActivity
+
+private const val FRAME_ADDED_WORK_MS = 20L
+private const val FRAME_BASELINE_WORK_MS = 10L
+private const val FRAME_COUNT = 100
+
+// NOTE: Keep in sync with FrameExperimentBenchmark!!
+private enum class FrameMode(val id: Int) {
+    Fast(0),
+    PrefetchEveryFrame(1),
+    WorkDuringEveryFrame(2),
+    PrefetchSomeFrames(3),
+}
+
+private class FrameExperimentView(context: Context, val mode: FrameMode) : View(context) {
+
+    init {
+        setOnClickListener {
+            remainingFrames = FRAME_COUNT - 1
+            invalidate()
+        }
+    }
+    var remainingFrames = 0
+
+    fun work(durationMs: Long = FRAME_ADDED_WORK_MS, label: String = "Added item work") {
+        Trace.beginSection(label)
+
+        // spin!
+        val endTime = System.nanoTime() + durationMs * 1_000_000
+        @Suppress("ControlFlowWithEmptyBody")
+        while (System.nanoTime() < endTime) {}
+
+        Trace.endSection()
+    }
+
+    val paintA = Paint().apply { setColor(Color.LTGRAY) }
+    val paintB = Paint().apply { setColor(Color.WHITE) }
+
+    override fun onDraw(canvas: Canvas) {
+        if (mode == FrameMode.WorkDuringEveryFrame) {
+            work()
+        }
+        super.onDraw(canvas)
+
+        work(durationMs = FRAME_BASELINE_WORK_MS, "Baseline work frame $remainingFrames")
+
+        // small rect to reduce flicker
+        canvas.drawRect(
+            0f, 0f, 200f, 200f,
+            if (remainingFrames % 2 == 0) paintA else paintB
+        )
+
+        if (remainingFrames >= 1) {
+            remainingFrames--
+            invalidate()
+
+            if (mode == FrameMode.PrefetchEveryFrame ||
+                (mode == FrameMode.PrefetchSomeFrames && remainingFrames % 5 == 0)) {
+                this.post {
+                    work()
+                }
+            }
+        }
+    }
+}
+
+class FrameExperimentActivity : ComponentActivity() {
+    override fun onCreate(savedInstanceState: Bundle?) {
+        super.onCreate(savedInstanceState)
+
+        val frameModeId = intent.getIntExtra(EXTRA_FRAME_MODE, defaultMode.id)
+        val frameMode = FrameMode.values().first { it.id == frameModeId }
+
+        setContentView(
+            FrameExperimentView(this, frameMode)
+        )
+    }
+
+    companion object {
+        const val EXTRA_FRAME_MODE = "FRAME_MODE"
+        private val defaultMode = FrameMode.Fast
+    }
+}
diff --git a/compose/integration-tests/macrobenchmark/src/main/java/androidx/compose/integration/macrobenchmark/FrameExperimentBenchmark.kt b/compose/integration-tests/macrobenchmark/src/main/java/androidx/compose/integration/macrobenchmark/FrameExperimentBenchmark.kt
new file mode 100644
index 0000000..36e8335
--- /dev/null
+++ b/compose/integration-tests/macrobenchmark/src/main/java/androidx/compose/integration/macrobenchmark/FrameExperimentBenchmark.kt
@@ -0,0 +1,88 @@
+/*
+ * Copyright 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.compose.integration.macrobenchmark
+
+import android.content.Intent
+import androidx.benchmark.macro.CompilationMode
+import androidx.benchmark.macro.ExperimentalMetricApi
+import androidx.benchmark.macro.FrameTimingGfxInfoMetric
+import androidx.benchmark.macro.FrameTimingMetric
+import androidx.benchmark.macro.junit4.MacrobenchmarkRule
+import androidx.test.ext.junit.runners.AndroidJUnit4
+import androidx.test.filters.LargeTest
+import androidx.testutils.createCompilationParams
+import org.junit.Rule
+import org.junit.Test
+import org.junit.runner.RunWith
+import org.junit.runners.Parameterized
+
+/**
+ * Benchmark for experimenting with synthetic frame patterns/durations and how
+ * they show up in metrics
+ */
+@LargeTest
+@RunWith(AndroidJUnit4::class)
+class FrameExperimentBenchmark {
+    @get:Rule
+    val benchmarkRule = MacrobenchmarkRule()
+
+    // NOTE: Keep in sync with FrameExperimentActivity!!
+    private enum class FrameMode(val id: Int) {
+        Fast(0),
+        PrefetchEveryFrame(1),
+        WorkDuringEveryFrame(2),
+        PrefetchSomeFrames(3),
+    }
+
+    @Test
+    fun fast() = benchmark(FrameMode.Fast)
+    @Test
+    fun prefetchEveryFrame() = benchmark(FrameMode.PrefetchEveryFrame)
+    @Test
+    fun workDuringEveryFrame() = benchmark(FrameMode.WorkDuringEveryFrame)
+    @Test
+    fun prefetchSomeFrames() = benchmark(FrameMode.PrefetchSomeFrames)
+
+    @OptIn(ExperimentalMetricApi::class)
+    private fun benchmark(mode: FrameMode) {
+        benchmarkRule.measureRepeated(
+            packageName = PACKAGE_NAME,
+            metrics = listOf(FrameTimingMetric(), FrameTimingGfxInfoMetric()),
+            compilationMode = CompilationMode.DEFAULT,
+            iterations = 1,
+            setupBlock = {
+                val intent = Intent()
+                intent.action = ACTION
+                intent.putExtra("FRAME_MODE", mode.id)
+                startActivityAndWait(intent)
+            }
+        ) {
+            device.click(device.displayWidth / 2, device.displayHeight / 2)
+            Thread.sleep(4_000) // empirically enough to produce expected frames
+        }
+    }
+
+    companion object {
+        private const val PACKAGE_NAME = "androidx.compose.integration.macrobenchmark.target"
+        private const val ACTION =
+            "androidx.compose.integration.macrobenchmark.target.FRAME_EXPERIMENT_ACTIVITY"
+
+        @Parameterized.Parameters(name = "compilation={0}")
+        @JvmStatic
+        fun parameters() = createCompilationParams()
+    }
+}
diff --git a/compose/lint/internal-lint-checks/src/main/java/androidx/compose/lint/UnnecessaryLambdaCreationDetector.kt b/compose/lint/internal-lint-checks/src/main/java/androidx/compose/lint/UnnecessaryLambdaCreationDetector.kt
index 67bb0c3..b84882b 100644
--- a/compose/lint/internal-lint-checks/src/main/java/androidx/compose/lint/UnnecessaryLambdaCreationDetector.kt
+++ b/compose/lint/internal-lint-checks/src/main/java/androidx/compose/lint/UnnecessaryLambdaCreationDetector.kt
@@ -28,12 +28,13 @@
 import com.android.tools.lint.detector.api.Severity
 import com.android.tools.lint.detector.api.SourceCodeScanner
 import com.intellij.psi.impl.source.PsiClassReferenceType
+import org.jetbrains.kotlin.psi.KtCallElement
+import org.jetbrains.uast.UBlockExpression
+import org.jetbrains.uast.UCallExpression
 import org.jetbrains.uast.ULambdaExpression
+import org.jetbrains.uast.UReturnExpression
+import org.jetbrains.uast.UUnknownExpression
 import org.jetbrains.uast.UVariable
-import org.jetbrains.uast.kotlin.KotlinUBlockExpression
-import org.jetbrains.uast.kotlin.KotlinUFunctionCallExpression
-import org.jetbrains.uast.kotlin.KotlinUImplicitReturnExpression
-import org.jetbrains.uast.kotlin.UnknownKotlinExpression
 import org.jetbrains.uast.skipParenthesizedExprDown
 import org.jetbrains.uast.toUElement
 import org.jetbrains.uast.tryResolve
@@ -81,14 +82,17 @@
     class UnnecessaryLambdaCreationHandler(private val context: JavaContext) : UElementHandler() {
 
         override fun visitLambdaExpression(node: ULambdaExpression) {
-            val expressions = (node.body as? KotlinUBlockExpression)?.expressions ?: return
+            val expressions = (node.body as? UBlockExpression)?.expressions ?: return
 
             if (expressions.size != 1) return
 
             val expression = when (val expr = expressions.first().skipParenthesizedExprDown()) {
-                is KotlinUFunctionCallExpression -> expr
-                is KotlinUImplicitReturnExpression ->
-                    expr.returnExpression as? KotlinUFunctionCallExpression
+                is UCallExpression -> expr
+                is UReturnExpression -> {
+                    if (expr.sourcePsi == null) { // implicit return
+                        expr.returnExpression as? UCallExpression
+                    } else null
+                }
                 else -> null
             } ?: return
 
@@ -97,7 +101,7 @@
             // We want to make sure this lambda is being invoked in the context of a function call,
             // and not as a property assignment - so we cast to KotlinUFunctionCallExpression to
             // filter out such cases.
-            val parentExpression = (node.uastParent as? KotlinUFunctionCallExpression) ?: return
+            val parentExpression = (node.uastParent as? UCallExpression) ?: return
 
             // If we can't resolve the parent call, then the parent function is defined in a
             // separate module, so we don't have the right metadata - and hence the argumentType
@@ -120,7 +124,8 @@
             val expectedComposable = node.isComposable
 
             // Try and get the UElement for the source of the lambda
-            val resolvedLambdaSource = expression.sourcePsi.calleeExpression?.toUElement()
+            val sourcePsi = expression.sourcePsi as? KtCallElement ?: return
+            val resolvedLambdaSource = sourcePsi.calleeExpression?.toUElement()
                 ?.tryResolve()?.toUElement()
                 // Sometimes the above will give us a method (representing the getter for a
                 // property), when the actual backing element is a property. Going to the source
@@ -132,7 +137,7 @@
                 // TODO: if the resolved source is a parameter in a local function, it
                 //  incorrectly returns an UnknownKotlinExpression instead of a UParameter
                 //  https://youtrack.jetbrains.com/issue/KTIJ-19125
-                is UnknownKotlinExpression -> return
+                is UUnknownExpression -> return
                 else -> error(parentExpression.asSourceString())
             }
 
diff --git a/compose/material/material-ripple/build.gradle b/compose/material/material-ripple/build.gradle
index 026f271..1bdfdec 100644
--- a/compose/material/material-ripple/build.gradle
+++ b/compose/material/material-ripple/build.gradle
@@ -40,12 +40,12 @@
         commonMain {
             dependencies {
                 implementation(libs.kotlinStdlibCommon)
-                api(project(":compose:foundation:foundation"))
-                api(project(":compose:runtime:runtime"))
+                api("androidx.compose.foundation:foundation:1.6.0")
+                api("androidx.compose.runtime:runtime:1.6.0")
 
                 implementation("androidx.collection:collection:1.4.0")
-                implementation(project(":compose:animation:animation"))
-                implementation(project(":compose:ui:ui-util"))
+                implementation("androidx.compose.animation:animation:1.6.0")
+                implementation("androidx.compose.ui:ui-util:1.6.0")
             }
         }
 
@@ -63,10 +63,6 @@
         skikoMain {
             dependsOn(commonMain)
             dependencies {
-                api(project(":compose:foundation:foundation"))
-                api(project(":compose:runtime:runtime"))
-                implementation(project(":compose:animation:animation"))
-                implementation(project(":compose:ui:ui-util"))
             }
         }
 
@@ -93,7 +89,7 @@
             dependsOn(jvmTest)
             dependencies {
                 implementation(project(":compose:test-utils"))
-
+                implementation(project(":compose:foundation:foundation"))
                 implementation(libs.testRules)
                 implementation(libs.testRunner)
                 implementation(libs.junit)
diff --git a/compose/material/material/build.gradle b/compose/material/material/build.gradle
index df79395..7234f90 100644
--- a/compose/material/material/build.gradle
+++ b/compose/material/material/build.gradle
@@ -41,17 +41,17 @@
         commonMain {
             dependencies {
                 implementation(libs.kotlinStdlibCommon)
-                api(project(":compose:animation:animation-core"))
+                api("androidx.compose.animation:animation-core:1.6.0")
                 api(project(":compose:foundation:foundation"))
                 api(project(":compose:material:material-icons-core"))
                 api(project(":compose:material:material-ripple"))
-                api(project(":compose:runtime:runtime"))
-                api(project(":compose:ui:ui"))
-                api(project(":compose:ui:ui-text"))
+                api("androidx.compose.runtime:runtime:1.6.0")
+                api("androidx.compose.ui:ui:1.6.0")
+                api("androidx.compose.ui:ui-text:1.6.0")
 
-                implementation(project(":compose:animation:animation"))
-                implementation(project(":compose:foundation:foundation-layout"))
-                implementation(project(":compose:ui:ui-util"))
+                implementation("androidx.compose.animation:animation:1.6.0")
+                implementation("androidx.compose.foundation:foundation-layout:1.6.0")
+                implementation("androidx.compose.ui:ui-util:1.6.0")
             }
         }
 
@@ -69,13 +69,6 @@
         skikoMain {
             dependsOn(commonMain)
             dependencies {
-                api(project(":compose:animation:animation-core"))
-                api(project(":compose:runtime:runtime"))
-                api(project(":compose:ui:ui"))
-                api(project(":compose:ui:ui-text"))
-                implementation(project(":compose:animation:animation"))
-                implementation(project(":compose:foundation:foundation-layout"))
-                implementation(project(":compose:ui:ui-util"))
             }
         }
 
@@ -111,6 +104,8 @@
             dependencies {
                 implementation(project(":compose:material:material:material-samples"))
                 implementation(project(":compose:test-utils"))
+                implementation("androidx.compose.ui:ui-test:1.6.0")
+                implementation("androidx.compose.ui:ui-test-junit4:1.6.0")
                 implementation(project(":test:screenshot:screenshot"))
 
                 implementation(libs.testRules)
diff --git a/compose/material/material/icons/generator/src/main/kotlin/androidx/compose/material/icons/generator/tasks/IconGenerationTask.kt b/compose/material/material/icons/generator/src/main/kotlin/androidx/compose/material/icons/generator/tasks/IconGenerationTask.kt
index 3626a13e..3787c6c 100644
--- a/compose/material/material/icons/generator/src/main/kotlin/androidx/compose/material/icons/generator/tasks/IconGenerationTask.kt
+++ b/compose/material/material/icons/generator/src/main/kotlin/androidx/compose/material/icons/generator/tasks/IconGenerationTask.kt
@@ -149,7 +149,7 @@
             if (isMpp) {
                 CoreIconGenerationTask.register(project, null)
             } else {
-                libraryExtension.libraryVariants.all { variant ->
+                libraryExtension.libraryVariants.configureEach { variant ->
                     CoreIconGenerationTask.register(project, variant)
                 }
             }
@@ -165,7 +165,7 @@
             project: Project,
             libraryExtension: LibraryExtension
         ) {
-            libraryExtension.libraryVariants.all { variant ->
+            libraryExtension.libraryVariants.configureEach { variant ->
                 if (variant.name == "release") {
                     ExtendedIconGenerationTask.register(project, variant)
                 }
@@ -189,7 +189,7 @@
             project: Project,
             libraryExtension: LibraryExtension
         ) {
-            libraryExtension.testVariants.all { variant ->
+            libraryExtension.testVariants.configureEach { variant ->
                 IconTestingGenerationTask.register(project, variant)
             }
         }
diff --git a/compose/material3/benchmark/src/androidTest/java/androidx/compose/material3/benchmark/NavigationSuiteScaffoldBenchmarkTest.kt b/compose/material3/benchmark/src/androidTest/java/androidx/compose/material3/benchmark/NavigationSuiteScaffoldBenchmarkTest.kt
index 21b602a..6b2b165 100644
--- a/compose/material3/benchmark/src/androidTest/java/androidx/compose/material3/benchmark/NavigationSuiteScaffoldBenchmarkTest.kt
+++ b/compose/material3/benchmark/src/androidTest/java/androidx/compose/material3/benchmark/NavigationSuiteScaffoldBenchmarkTest.kt
@@ -19,8 +19,8 @@
 import androidx.compose.foundation.layout.Spacer
 import androidx.compose.foundation.layout.size
 import androidx.compose.material3.MaterialTheme
-import androidx.compose.material3.adaptive.navigation.suite.ExperimentalMaterial3AdaptiveNavigationSuiteApi
-import androidx.compose.material3.adaptive.navigation.suite.NavigationSuiteScaffold
+import androidx.compose.material3.adaptive.navigationsuite.ExperimentalMaterial3AdaptiveNavigationSuiteApi
+import androidx.compose.material3.adaptive.navigationsuite.NavigationSuiteScaffold
 import androidx.compose.runtime.Composable
 import androidx.compose.runtime.MutableIntState
 import androidx.compose.runtime.mutableIntStateOf
diff --git a/compose/material3/material3-adaptive-navigation-suite/androidx-compose-material3-adaptive-navigation-suite-documentation.md b/compose/material3/material3-adaptive-navigation-suite/androidx-compose-material3-adaptive-navigation-suite-documentation.md
index 645b7b6..92347e5 100644
--- a/compose/material3/material3-adaptive-navigation-suite/androidx-compose-material3-adaptive-navigation-suite-documentation.md
+++ b/compose/material3/material3-adaptive-navigation-suite/androidx-compose-material3-adaptive-navigation-suite-documentation.md
@@ -2,4 +2,4 @@
 
 Compose Material Adaptive Navigation Suite
 
-# Package androidx.compose.material3.adaptive.navigation.suite
\ No newline at end of file
+# Package androidx.compose.material3.adaptive.navigationsuite
\ No newline at end of file
diff --git a/compose/material3/material3-adaptive-navigation-suite/api/current.txt b/compose/material3/material3-adaptive-navigation-suite/api/current.txt
index a87d1e4..a4d597b 100644
--- a/compose/material3/material3-adaptive-navigation-suite/api/current.txt
+++ b/compose/material3/material3-adaptive-navigation-suite/api/current.txt
@@ -1,10 +1,10 @@
 // Signature format: 4.0
-package androidx.compose.material3.adaptive.navigation.suite {
+package androidx.compose.material3.adaptive.navigationsuite {
 
   @SuppressCompatibility @kotlin.RequiresOptIn(message="This material3-adaptive-navigation-suite API is experimental and is likely to" + "change or to be removed in the future.") @kotlin.annotation.Retention(kotlin.annotation.AnnotationRetention.BINARY) public @interface ExperimentalMaterial3AdaptiveNavigationSuiteApi {
   }
 
-  @SuppressCompatibility @androidx.compose.material3.adaptive.navigation.suite.ExperimentalMaterial3AdaptiveNavigationSuiteApi public final class NavigationSuiteColors {
+  @SuppressCompatibility @androidx.compose.material3.adaptive.navigationsuite.ExperimentalMaterial3AdaptiveNavigationSuiteApi public final class NavigationSuiteColors {
     method public long getNavigationBarContainerColor();
     method public long getNavigationBarContentColor();
     method public long getNavigationDrawerContainerColor();
@@ -19,12 +19,12 @@
     property public final long navigationRailContentColor;
   }
 
-  @SuppressCompatibility @androidx.compose.material3.adaptive.navigation.suite.ExperimentalMaterial3AdaptiveNavigationSuiteApi public final class NavigationSuiteDefaults {
-    method @androidx.compose.runtime.Composable public androidx.compose.material3.adaptive.navigation.suite.NavigationSuiteColors colors(optional long navigationBarContainerColor, optional long navigationBarContentColor, optional long navigationRailContainerColor, optional long navigationRailContentColor, optional long navigationDrawerContainerColor, optional long navigationDrawerContentColor);
-    field public static final androidx.compose.material3.adaptive.navigation.suite.NavigationSuiteDefaults INSTANCE;
+  @SuppressCompatibility @androidx.compose.material3.adaptive.navigationsuite.ExperimentalMaterial3AdaptiveNavigationSuiteApi public final class NavigationSuiteDefaults {
+    method @androidx.compose.runtime.Composable public androidx.compose.material3.adaptive.navigationsuite.NavigationSuiteColors colors(optional long navigationBarContainerColor, optional long navigationBarContentColor, optional long navigationRailContainerColor, optional long navigationRailContentColor, optional long navigationDrawerContainerColor, optional long navigationDrawerContentColor);
+    field public static final androidx.compose.material3.adaptive.navigationsuite.NavigationSuiteDefaults INSTANCE;
   }
 
-  @SuppressCompatibility @androidx.compose.material3.adaptive.navigation.suite.ExperimentalMaterial3AdaptiveNavigationSuiteApi public final class NavigationSuiteItemColors {
+  @SuppressCompatibility @androidx.compose.material3.adaptive.navigationsuite.ExperimentalMaterial3AdaptiveNavigationSuiteApi public final class NavigationSuiteItemColors {
     method public androidx.compose.material3.NavigationBarItemColors getNavigationBarItemColors();
     method public androidx.compose.material3.NavigationDrawerItemColors getNavigationDrawerItemColors();
     method public androidx.compose.material3.NavigationRailItemColors getNavigationRailItemColors();
@@ -33,23 +33,23 @@
     property public final androidx.compose.material3.NavigationRailItemColors navigationRailItemColors;
   }
 
-  @SuppressCompatibility @androidx.compose.material3.adaptive.navigation.suite.ExperimentalMaterial3AdaptiveNavigationSuiteApi public final class NavigationSuiteScaffoldDefaults {
+  @SuppressCompatibility @androidx.compose.material3.adaptive.navigationsuite.ExperimentalMaterial3AdaptiveNavigationSuiteApi public final class NavigationSuiteScaffoldDefaults {
     method public String calculateFromAdaptiveInfo(androidx.compose.material3.adaptive.WindowAdaptiveInfo adaptiveInfo);
-    field public static final androidx.compose.material3.adaptive.navigation.suite.NavigationSuiteScaffoldDefaults INSTANCE;
+    field public static final androidx.compose.material3.adaptive.navigationsuite.NavigationSuiteScaffoldDefaults INSTANCE;
   }
 
   public final class NavigationSuiteScaffoldKt {
-    method @SuppressCompatibility @androidx.compose.material3.adaptive.navigation.suite.ExperimentalMaterial3AdaptiveNavigationSuiteApi @androidx.compose.runtime.Composable public static void NavigationSuite(optional androidx.compose.ui.Modifier modifier, optional String layoutType, optional androidx.compose.material3.adaptive.navigation.suite.NavigationSuiteColors colors, kotlin.jvm.functions.Function1<? super androidx.compose.material3.adaptive.navigation.suite.NavigationSuiteScope,kotlin.Unit> content);
-    method @SuppressCompatibility @androidx.compose.material3.adaptive.navigation.suite.ExperimentalMaterial3AdaptiveNavigationSuiteApi @androidx.compose.runtime.Composable public static void NavigationSuiteScaffold(kotlin.jvm.functions.Function1<? super androidx.compose.material3.adaptive.navigation.suite.NavigationSuiteScope,kotlin.Unit> navigationSuiteItems, optional androidx.compose.ui.Modifier modifier, optional String layoutType, optional androidx.compose.material3.adaptive.navigation.suite.NavigationSuiteColors navigationSuiteColors, optional long containerColor, optional long contentColor, optional kotlin.jvm.functions.Function0<kotlin.Unit> content);
-    method @SuppressCompatibility @androidx.compose.material3.adaptive.navigation.suite.ExperimentalMaterial3AdaptiveNavigationSuiteApi @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);
+    method @SuppressCompatibility @androidx.compose.material3.adaptive.navigationsuite.ExperimentalMaterial3AdaptiveNavigationSuiteApi @androidx.compose.runtime.Composable public static void NavigationSuite(optional androidx.compose.ui.Modifier modifier, optional String layoutType, optional androidx.compose.material3.adaptive.navigationsuite.NavigationSuiteColors colors, kotlin.jvm.functions.Function1<? super androidx.compose.material3.adaptive.navigationsuite.NavigationSuiteScope,kotlin.Unit> content);
+    method @SuppressCompatibility @androidx.compose.material3.adaptive.navigationsuite.ExperimentalMaterial3AdaptiveNavigationSuiteApi @androidx.compose.runtime.Composable public static void NavigationSuiteScaffold(kotlin.jvm.functions.Function1<? super androidx.compose.material3.adaptive.navigationsuite.NavigationSuiteScope,kotlin.Unit> navigationSuiteItems, optional androidx.compose.ui.Modifier modifier, optional String layoutType, optional androidx.compose.material3.adaptive.navigationsuite.NavigationSuiteColors navigationSuiteColors, optional long containerColor, optional long contentColor, optional kotlin.jvm.functions.Function0<kotlin.Unit> content);
+    method @SuppressCompatibility @androidx.compose.material3.adaptive.navigationsuite.ExperimentalMaterial3AdaptiveNavigationSuiteApi @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.navigation.suite.ExperimentalMaterial3AdaptiveNavigationSuiteApi public interface NavigationSuiteScope {
-    method public void item(boolean selected, kotlin.jvm.functions.Function0<kotlin.Unit> onClick, kotlin.jvm.functions.Function0<kotlin.Unit> icon, optional androidx.compose.ui.Modifier modifier, optional boolean enabled, optional kotlin.jvm.functions.Function0<kotlin.Unit>? label, optional boolean alwaysShowLabel, optional kotlin.jvm.functions.Function0<kotlin.Unit>? badge, optional androidx.compose.material3.adaptive.navigation.suite.NavigationSuiteItemColors? colors, optional androidx.compose.foundation.interaction.MutableInteractionSource? interactionSource);
+  @SuppressCompatibility @androidx.compose.material3.adaptive.navigationsuite.ExperimentalMaterial3AdaptiveNavigationSuiteApi public interface NavigationSuiteScope {
+    method public void item(boolean selected, kotlin.jvm.functions.Function0<kotlin.Unit> onClick, kotlin.jvm.functions.Function0<kotlin.Unit> icon, optional androidx.compose.ui.Modifier modifier, optional boolean enabled, optional kotlin.jvm.functions.Function0<kotlin.Unit>? label, optional boolean alwaysShowLabel, optional kotlin.jvm.functions.Function0<kotlin.Unit>? badge, optional androidx.compose.material3.adaptive.navigationsuite.NavigationSuiteItemColors? colors, optional androidx.compose.foundation.interaction.MutableInteractionSource? interactionSource);
   }
 
-  @SuppressCompatibility @androidx.compose.material3.adaptive.navigation.suite.ExperimentalMaterial3AdaptiveNavigationSuiteApi @kotlin.jvm.JvmInline public final value class NavigationSuiteType {
-    field public static final androidx.compose.material3.adaptive.navigation.suite.NavigationSuiteType.Companion Companion;
+  @SuppressCompatibility @androidx.compose.material3.adaptive.navigationsuite.ExperimentalMaterial3AdaptiveNavigationSuiteApi @kotlin.jvm.JvmInline public final value class NavigationSuiteType {
+    field public static final androidx.compose.material3.adaptive.navigationsuite.NavigationSuiteType.Companion Companion;
   }
 
   public static final class NavigationSuiteType.Companion {
diff --git a/compose/material3/material3-adaptive-navigation-suite/api/restricted_current.txt b/compose/material3/material3-adaptive-navigation-suite/api/restricted_current.txt
index a87d1e4..a4d597b 100644
--- a/compose/material3/material3-adaptive-navigation-suite/api/restricted_current.txt
+++ b/compose/material3/material3-adaptive-navigation-suite/api/restricted_current.txt
@@ -1,10 +1,10 @@
 // Signature format: 4.0
-package androidx.compose.material3.adaptive.navigation.suite {
+package androidx.compose.material3.adaptive.navigationsuite {
 
   @SuppressCompatibility @kotlin.RequiresOptIn(message="This material3-adaptive-navigation-suite API is experimental and is likely to" + "change or to be removed in the future.") @kotlin.annotation.Retention(kotlin.annotation.AnnotationRetention.BINARY) public @interface ExperimentalMaterial3AdaptiveNavigationSuiteApi {
   }
 
-  @SuppressCompatibility @androidx.compose.material3.adaptive.navigation.suite.ExperimentalMaterial3AdaptiveNavigationSuiteApi public final class NavigationSuiteColors {
+  @SuppressCompatibility @androidx.compose.material3.adaptive.navigationsuite.ExperimentalMaterial3AdaptiveNavigationSuiteApi public final class NavigationSuiteColors {
     method public long getNavigationBarContainerColor();
     method public long getNavigationBarContentColor();
     method public long getNavigationDrawerContainerColor();
@@ -19,12 +19,12 @@
     property public final long navigationRailContentColor;
   }
 
-  @SuppressCompatibility @androidx.compose.material3.adaptive.navigation.suite.ExperimentalMaterial3AdaptiveNavigationSuiteApi public final class NavigationSuiteDefaults {
-    method @androidx.compose.runtime.Composable public androidx.compose.material3.adaptive.navigation.suite.NavigationSuiteColors colors(optional long navigationBarContainerColor, optional long navigationBarContentColor, optional long navigationRailContainerColor, optional long navigationRailContentColor, optional long navigationDrawerContainerColor, optional long navigationDrawerContentColor);
-    field public static final androidx.compose.material3.adaptive.navigation.suite.NavigationSuiteDefaults INSTANCE;
+  @SuppressCompatibility @androidx.compose.material3.adaptive.navigationsuite.ExperimentalMaterial3AdaptiveNavigationSuiteApi public final class NavigationSuiteDefaults {
+    method @androidx.compose.runtime.Composable public androidx.compose.material3.adaptive.navigationsuite.NavigationSuiteColors colors(optional long navigationBarContainerColor, optional long navigationBarContentColor, optional long navigationRailContainerColor, optional long navigationRailContentColor, optional long navigationDrawerContainerColor, optional long navigationDrawerContentColor);
+    field public static final androidx.compose.material3.adaptive.navigationsuite.NavigationSuiteDefaults INSTANCE;
   }
 
-  @SuppressCompatibility @androidx.compose.material3.adaptive.navigation.suite.ExperimentalMaterial3AdaptiveNavigationSuiteApi public final class NavigationSuiteItemColors {
+  @SuppressCompatibility @androidx.compose.material3.adaptive.navigationsuite.ExperimentalMaterial3AdaptiveNavigationSuiteApi public final class NavigationSuiteItemColors {
     method public androidx.compose.material3.NavigationBarItemColors getNavigationBarItemColors();
     method public androidx.compose.material3.NavigationDrawerItemColors getNavigationDrawerItemColors();
     method public androidx.compose.material3.NavigationRailItemColors getNavigationRailItemColors();
@@ -33,23 +33,23 @@
     property public final androidx.compose.material3.NavigationRailItemColors navigationRailItemColors;
   }
 
-  @SuppressCompatibility @androidx.compose.material3.adaptive.navigation.suite.ExperimentalMaterial3AdaptiveNavigationSuiteApi public final class NavigationSuiteScaffoldDefaults {
+  @SuppressCompatibility @androidx.compose.material3.adaptive.navigationsuite.ExperimentalMaterial3AdaptiveNavigationSuiteApi public final class NavigationSuiteScaffoldDefaults {
     method public String calculateFromAdaptiveInfo(androidx.compose.material3.adaptive.WindowAdaptiveInfo adaptiveInfo);
-    field public static final androidx.compose.material3.adaptive.navigation.suite.NavigationSuiteScaffoldDefaults INSTANCE;
+    field public static final androidx.compose.material3.adaptive.navigationsuite.NavigationSuiteScaffoldDefaults INSTANCE;
   }
 
   public final class NavigationSuiteScaffoldKt {
-    method @SuppressCompatibility @androidx.compose.material3.adaptive.navigation.suite.ExperimentalMaterial3AdaptiveNavigationSuiteApi @androidx.compose.runtime.Composable public static void NavigationSuite(optional androidx.compose.ui.Modifier modifier, optional String layoutType, optional androidx.compose.material3.adaptive.navigation.suite.NavigationSuiteColors colors, kotlin.jvm.functions.Function1<? super androidx.compose.material3.adaptive.navigation.suite.NavigationSuiteScope,kotlin.Unit> content);
-    method @SuppressCompatibility @androidx.compose.material3.adaptive.navigation.suite.ExperimentalMaterial3AdaptiveNavigationSuiteApi @androidx.compose.runtime.Composable public static void NavigationSuiteScaffold(kotlin.jvm.functions.Function1<? super androidx.compose.material3.adaptive.navigation.suite.NavigationSuiteScope,kotlin.Unit> navigationSuiteItems, optional androidx.compose.ui.Modifier modifier, optional String layoutType, optional androidx.compose.material3.adaptive.navigation.suite.NavigationSuiteColors navigationSuiteColors, optional long containerColor, optional long contentColor, optional kotlin.jvm.functions.Function0<kotlin.Unit> content);
-    method @SuppressCompatibility @androidx.compose.material3.adaptive.navigation.suite.ExperimentalMaterial3AdaptiveNavigationSuiteApi @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);
+    method @SuppressCompatibility @androidx.compose.material3.adaptive.navigationsuite.ExperimentalMaterial3AdaptiveNavigationSuiteApi @androidx.compose.runtime.Composable public static void NavigationSuite(optional androidx.compose.ui.Modifier modifier, optional String layoutType, optional androidx.compose.material3.adaptive.navigationsuite.NavigationSuiteColors colors, kotlin.jvm.functions.Function1<? super androidx.compose.material3.adaptive.navigationsuite.NavigationSuiteScope,kotlin.Unit> content);
+    method @SuppressCompatibility @androidx.compose.material3.adaptive.navigationsuite.ExperimentalMaterial3AdaptiveNavigationSuiteApi @androidx.compose.runtime.Composable public static void NavigationSuiteScaffold(kotlin.jvm.functions.Function1<? super androidx.compose.material3.adaptive.navigationsuite.NavigationSuiteScope,kotlin.Unit> navigationSuiteItems, optional androidx.compose.ui.Modifier modifier, optional String layoutType, optional androidx.compose.material3.adaptive.navigationsuite.NavigationSuiteColors navigationSuiteColors, optional long containerColor, optional long contentColor, optional kotlin.jvm.functions.Function0<kotlin.Unit> content);
+    method @SuppressCompatibility @androidx.compose.material3.adaptive.navigationsuite.ExperimentalMaterial3AdaptiveNavigationSuiteApi @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.navigation.suite.ExperimentalMaterial3AdaptiveNavigationSuiteApi public interface NavigationSuiteScope {
-    method public void item(boolean selected, kotlin.jvm.functions.Function0<kotlin.Unit> onClick, kotlin.jvm.functions.Function0<kotlin.Unit> icon, optional androidx.compose.ui.Modifier modifier, optional boolean enabled, optional kotlin.jvm.functions.Function0<kotlin.Unit>? label, optional boolean alwaysShowLabel, optional kotlin.jvm.functions.Function0<kotlin.Unit>? badge, optional androidx.compose.material3.adaptive.navigation.suite.NavigationSuiteItemColors? colors, optional androidx.compose.foundation.interaction.MutableInteractionSource? interactionSource);
+  @SuppressCompatibility @androidx.compose.material3.adaptive.navigationsuite.ExperimentalMaterial3AdaptiveNavigationSuiteApi public interface NavigationSuiteScope {
+    method public void item(boolean selected, kotlin.jvm.functions.Function0<kotlin.Unit> onClick, kotlin.jvm.functions.Function0<kotlin.Unit> icon, optional androidx.compose.ui.Modifier modifier, optional boolean enabled, optional kotlin.jvm.functions.Function0<kotlin.Unit>? label, optional boolean alwaysShowLabel, optional kotlin.jvm.functions.Function0<kotlin.Unit>? badge, optional androidx.compose.material3.adaptive.navigationsuite.NavigationSuiteItemColors? colors, optional androidx.compose.foundation.interaction.MutableInteractionSource? interactionSource);
   }
 
-  @SuppressCompatibility @androidx.compose.material3.adaptive.navigation.suite.ExperimentalMaterial3AdaptiveNavigationSuiteApi @kotlin.jvm.JvmInline public final value class NavigationSuiteType {
-    field public static final androidx.compose.material3.adaptive.navigation.suite.NavigationSuiteType.Companion Companion;
+  @SuppressCompatibility @androidx.compose.material3.adaptive.navigationsuite.ExperimentalMaterial3AdaptiveNavigationSuiteApi @kotlin.jvm.JvmInline public final value class NavigationSuiteType {
+    field public static final androidx.compose.material3.adaptive.navigationsuite.NavigationSuiteType.Companion Companion;
   }
 
   public static final class NavigationSuiteType.Companion {
diff --git a/compose/material3/material3-adaptive-navigation-suite/build.gradle b/compose/material3/material3-adaptive-navigation-suite/build.gradle
index ae2c546..249b82a 100644
--- a/compose/material3/material3-adaptive-navigation-suite/build.gradle
+++ b/compose/material3/material3-adaptive-navigation-suite/build.gradle
@@ -45,8 +45,8 @@
                 implementation(libs.kotlinStdlibCommon)
                 implementation(project(":compose:material3:material3"))
                 implementation(project(":compose:material3:material3-adaptive"))
-                implementation(project(":compose:material3:material3-window-size-class"))
-                implementation(project(":compose:ui:ui-util"))
+                implementation("androidx.compose.material3:material3-window-size-class:1.2.0-rc01")
+                implementation("androidx.compose.ui:ui-util:1.6.0-rc01")
             }
         }
 
@@ -108,7 +108,7 @@
 }
 
 android {
-    namespace "androidx.compose.material3.adaptive.navigation.suite"
+    namespace "androidx.compose.material3.adaptive.navigationsuite"
 }
 
 androidx {
diff --git a/compose/material3/material3-adaptive-navigation-suite/samples/build.gradle b/compose/material3/material3-adaptive-navigation-suite/samples/build.gradle
index 653909e..088b2b6 100644
--- a/compose/material3/material3-adaptive-navigation-suite/samples/build.gradle
+++ b/compose/material3/material3-adaptive-navigation-suite/samples/build.gradle
@@ -36,13 +36,13 @@
 
     compileOnly(project(":annotation:annotation-sampled"))
 
-    implementation(project(":compose:foundation:foundation"))
-    implementation(project(":compose:foundation:foundation-layout"))
+    implementation("androidx.compose.foundation:foundation:1.6.0-rc01")
+    implementation("androidx.compose.foundation:foundation-layout:1.6.0-rc01")
     implementation(project(":compose:material3:material3"))
     implementation(project(":compose:material3:material3-adaptive"))
     implementation(project(":compose:material3:material3-adaptive-navigation-suite"))
     implementation(project(":compose:material3:material3-window-size-class"))
-    implementation(project(":compose:ui:ui-util"))
+    implementation("androidx.compose.ui:ui-util:1.6.0-rc01")
     implementation("androidx.compose.ui:ui-tooling-preview:1.4.1")
 
     debugImplementation("androidx.compose.ui:ui-tooling:1.4.1")
@@ -57,5 +57,5 @@
 }
 
 android {
-    namespace "androidx.compose.material3.adaptive.navigation.suite.samples"
+    namespace "androidx.compose.material3.adaptive.navigationsuite.samples"
 }
diff --git a/compose/material3/material3-adaptive-navigation-suite/samples/src/main/java/androidx/compose/material3/adaptive/navigation/suite/samples/NavigationSuiteScaffoldSamples.kt b/compose/material3/material3-adaptive-navigation-suite/samples/src/main/java/androidx/compose/material3/adaptive/navigationsuite/samples/NavigationSuiteScaffoldSamples.kt
similarity index 89%
rename from compose/material3/material3-adaptive-navigation-suite/samples/src/main/java/androidx/compose/material3/adaptive/navigation/suite/samples/NavigationSuiteScaffoldSamples.kt
rename to compose/material3/material3-adaptive-navigation-suite/samples/src/main/java/androidx/compose/material3/adaptive/navigationsuite/samples/NavigationSuiteScaffoldSamples.kt
index ed9664a..0b5cf96 100644
--- a/compose/material3/material3-adaptive-navigation-suite/samples/src/main/java/androidx/compose/material3/adaptive/navigation/suite/samples/NavigationSuiteScaffoldSamples.kt
+++ b/compose/material3/material3-adaptive-navigation-suite/samples/src/main/java/androidx/compose/material3/adaptive/navigationsuite/samples/NavigationSuiteScaffoldSamples.kt
@@ -14,7 +14,7 @@
  * limitations under the License.
  */
 
-package androidx.compose.material3.adaptive.navigation.suite.samples
+package androidx.compose.material3.adaptive.navigationsuite.samples
 
 import androidx.annotation.Sampled
 import androidx.compose.foundation.layout.padding
@@ -24,10 +24,10 @@
 import androidx.compose.material3.Text
 import androidx.compose.material3.adaptive.ExperimentalMaterial3AdaptiveApi
 import androidx.compose.material3.adaptive.currentWindowAdaptiveInfo
-import androidx.compose.material3.adaptive.navigation.suite.ExperimentalMaterial3AdaptiveNavigationSuiteApi
-import androidx.compose.material3.adaptive.navigation.suite.NavigationSuiteScaffold
-import androidx.compose.material3.adaptive.navigation.suite.NavigationSuiteScaffoldDefaults
-import androidx.compose.material3.adaptive.navigation.suite.NavigationSuiteType
+import androidx.compose.material3.adaptive.navigationsuite.ExperimentalMaterial3AdaptiveNavigationSuiteApi
+import androidx.compose.material3.adaptive.navigationsuite.NavigationSuiteScaffold
+import androidx.compose.material3.adaptive.navigationsuite.NavigationSuiteScaffoldDefaults
+import androidx.compose.material3.adaptive.navigationsuite.NavigationSuiteType
 import androidx.compose.material3.windowsizeclass.WindowWidthSizeClass
 import androidx.compose.runtime.Composable
 import androidx.compose.runtime.getValue
diff --git a/compose/material3/material3-adaptive-navigation-suite/src/androidInstrumentedTest/kotlin/androidx/compose/material3/adaptive-navigation-suite/NavigationSuiteScaffoldTest.kt b/compose/material3/material3-adaptive-navigation-suite/src/androidInstrumentedTest/kotlin/androidx/compose/material3/adaptive/navigationsuite/NavigationSuiteScaffoldTest.kt
similarity index 98%
rename from compose/material3/material3-adaptive-navigation-suite/src/androidInstrumentedTest/kotlin/androidx/compose/material3/adaptive-navigation-suite/NavigationSuiteScaffoldTest.kt
rename to compose/material3/material3-adaptive-navigation-suite/src/androidInstrumentedTest/kotlin/androidx/compose/material3/adaptive/navigationsuite/NavigationSuiteScaffoldTest.kt
index 7cbf1c3..737aa7f 100644
--- a/compose/material3/material3-adaptive-navigation-suite/src/androidInstrumentedTest/kotlin/androidx/compose/material3/adaptive-navigation-suite/NavigationSuiteScaffoldTest.kt
+++ b/compose/material3/material3-adaptive-navigation-suite/src/androidInstrumentedTest/kotlin/androidx/compose/material3/adaptive/navigationsuite/NavigationSuiteScaffoldTest.kt
@@ -14,7 +14,7 @@
  * limitations under the License.
  */
 
-package androidx.compose.material3.adaptive.navigation.suite
+package androidx.compose.material3.adaptive.navigationsuite
 
 import androidx.compose.foundation.layout.fillMaxSize
 import androidx.compose.material3.Surface
diff --git a/compose/material3/material3-adaptive-navigation-suite/src/androidInstrumentedTest/kotlin/androidx/compose/material3/adaptive-navigation-suite/NavigationSuiteTest.kt b/compose/material3/material3-adaptive-navigation-suite/src/androidInstrumentedTest/kotlin/androidx/compose/material3/adaptive/navigationsuite/NavigationSuiteTest.kt
similarity index 96%
rename from compose/material3/material3-adaptive-navigation-suite/src/androidInstrumentedTest/kotlin/androidx/compose/material3/adaptive-navigation-suite/NavigationSuiteTest.kt
rename to compose/material3/material3-adaptive-navigation-suite/src/androidInstrumentedTest/kotlin/androidx/compose/material3/adaptive/navigationsuite/NavigationSuiteTest.kt
index 3dffee7..df34599 100644
--- a/compose/material3/material3-adaptive-navigation-suite/src/androidInstrumentedTest/kotlin/androidx/compose/material3/adaptive-navigation-suite/NavigationSuiteTest.kt
+++ b/compose/material3/material3-adaptive-navigation-suite/src/androidInstrumentedTest/kotlin/androidx/compose/material3/adaptive/navigationsuite/NavigationSuiteTest.kt
@@ -14,7 +14,7 @@
  * limitations under the License.
  */
 
-package androidx.compose.material3.adaptive.navigation.suite
+package androidx.compose.material3.adaptive.navigationsuite
 
 import androidx.compose.runtime.Composable
 import androidx.compose.ui.Modifier
diff --git a/compose/material3/material3-adaptive-navigation-suite/src/androidMain/kotlin/androidx/compose/material3/adaptive/navigation-suite/NavigationSuiteScaffold.android.kt b/compose/material3/material3-adaptive-navigation-suite/src/androidMain/kotlin/androidx/compose/material3/adaptive/navigationsuite/NavigationSuiteScaffold.android.kt
similarity index 94%
rename from compose/material3/material3-adaptive-navigation-suite/src/androidMain/kotlin/androidx/compose/material3/adaptive/navigation-suite/NavigationSuiteScaffold.android.kt
rename to compose/material3/material3-adaptive-navigation-suite/src/androidMain/kotlin/androidx/compose/material3/adaptive/navigationsuite/NavigationSuiteScaffold.android.kt
index be34e16..4af953a 100644
--- a/compose/material3/material3-adaptive-navigation-suite/src/androidMain/kotlin/androidx/compose/material3/adaptive/navigation-suite/NavigationSuiteScaffold.android.kt
+++ b/compose/material3/material3-adaptive-navigation-suite/src/androidMain/kotlin/androidx/compose/material3/adaptive/navigationsuite/NavigationSuiteScaffold.android.kt
@@ -14,7 +14,7 @@
  * limitations under the License.
  */
 
-package androidx.compose.material3.adaptive.navigation.suite
+package androidx.compose.material3.adaptive.navigationsuite
 
 import androidx.compose.material3.adaptive.ExperimentalMaterial3AdaptiveApi
 import androidx.compose.material3.adaptive.WindowAdaptiveInfo
diff --git a/compose/material3/material3-adaptive-navigation-suite/src/androidUnitTest/kotlin/androidx/compose/material3/adaptive/navigation-suite/NavigationSuiteScaffoldTest.kt b/compose/material3/material3-adaptive-navigation-suite/src/androidUnitTest/kotlin/androidx/compose/material3/adaptive/navigationsuite/NavigationSuiteScaffoldTest.kt
similarity index 98%
rename from compose/material3/material3-adaptive-navigation-suite/src/androidUnitTest/kotlin/androidx/compose/material3/adaptive/navigation-suite/NavigationSuiteScaffoldTest.kt
rename to compose/material3/material3-adaptive-navigation-suite/src/androidUnitTest/kotlin/androidx/compose/material3/adaptive/navigationsuite/NavigationSuiteScaffoldTest.kt
index 745cccb..c349a8e 100644
--- a/compose/material3/material3-adaptive-navigation-suite/src/androidUnitTest/kotlin/androidx/compose/material3/adaptive/navigation-suite/NavigationSuiteScaffoldTest.kt
+++ b/compose/material3/material3-adaptive-navigation-suite/src/androidUnitTest/kotlin/androidx/compose/material3/adaptive/navigationsuite/NavigationSuiteScaffoldTest.kt
@@ -14,7 +14,7 @@
  * limitations under the License.
  */
 
-package androidx.compose.material3.adaptive.navigation.suite
+package androidx.compose.material3.adaptive.navigationsuite
 
 import androidx.compose.material3.adaptive.ExperimentalMaterial3AdaptiveApi
 import androidx.compose.material3.adaptive.Posture
diff --git a/compose/material3/material3-adaptive-navigation-suite/src/commonMain/kotlin/androidx/compose/material3/adaptive/navigation-suite/ExperimentalMaterial3AdaptiveComponentsApi.kt b/compose/material3/material3-adaptive-navigation-suite/src/commonMain/kotlin/androidx/compose/material3/adaptive/navigationsuite/ExperimentalMaterial3AdaptiveComponentsApi.kt
similarity index 93%
rename from compose/material3/material3-adaptive-navigation-suite/src/commonMain/kotlin/androidx/compose/material3/adaptive/navigation-suite/ExperimentalMaterial3AdaptiveComponentsApi.kt
rename to compose/material3/material3-adaptive-navigation-suite/src/commonMain/kotlin/androidx/compose/material3/adaptive/navigationsuite/ExperimentalMaterial3AdaptiveComponentsApi.kt
index 054c8e5..44f0baa 100644
--- a/compose/material3/material3-adaptive-navigation-suite/src/commonMain/kotlin/androidx/compose/material3/adaptive/navigation-suite/ExperimentalMaterial3AdaptiveComponentsApi.kt
+++ b/compose/material3/material3-adaptive-navigation-suite/src/commonMain/kotlin/androidx/compose/material3/adaptive/navigationsuite/ExperimentalMaterial3AdaptiveComponentsApi.kt
@@ -14,7 +14,7 @@
  * limitations under the License.
  */
 
-package androidx.compose.material3.adaptive.navigation.suite
+package androidx.compose.material3.adaptive.navigationsuite
 
 @RequiresOptIn(
     "This material3-adaptive-navigation-suite API is experimental and is likely to" +
diff --git a/compose/material3/material3-adaptive-navigation-suite/src/commonMain/kotlin/androidx/compose/material3/adaptive/navigation-suite/NavigationSuiteScaffold.kt b/compose/material3/material3-adaptive-navigation-suite/src/commonMain/kotlin/androidx/compose/material3/adaptive/navigationsuite/NavigationSuiteScaffold.kt
similarity index 98%
rename from compose/material3/material3-adaptive-navigation-suite/src/commonMain/kotlin/androidx/compose/material3/adaptive/navigation-suite/NavigationSuiteScaffold.kt
rename to compose/material3/material3-adaptive-navigation-suite/src/commonMain/kotlin/androidx/compose/material3/adaptive/navigationsuite/NavigationSuiteScaffold.kt
index 4787981..b0c6f78 100644
--- a/compose/material3/material3-adaptive-navigation-suite/src/commonMain/kotlin/androidx/compose/material3/adaptive/navigation-suite/NavigationSuiteScaffold.kt
+++ b/compose/material3/material3-adaptive-navigation-suite/src/commonMain/kotlin/androidx/compose/material3/adaptive/navigationsuite/NavigationSuiteScaffold.kt
@@ -14,7 +14,7 @@
  * limitations under the License.
  */
 
-package androidx.compose.material3.adaptive.navigation.suite
+package androidx.compose.material3.adaptive.navigationsuite
 
 import androidx.compose.foundation.interaction.Interaction
 import androidx.compose.foundation.interaction.MutableInteractionSource
@@ -65,9 +65,9 @@
  * navigation component on the screen according to the current [NavigationSuiteType].
  *
  * Example default usage:
- * @sample androidx.compose.material3.adaptive.navigation.suite.samples.NavigationSuiteScaffoldSample
+ * @sample androidx.compose.material3.adaptive.navigationsuite.samples.NavigationSuiteScaffoldSample
  * Example custom configuration usage:
- * @sample androidx.compose.material3.adaptive.navigation.suite.samples.NavigationSuiteScaffoldCustomConfigSample
+ * @sample androidx.compose.material3.adaptive.navigationsuite.samples.NavigationSuiteScaffoldCustomConfigSample
  *
  * @param navigationSuiteItems the navigation items to be displayed
  * @param modifier the [Modifier] to be applied to the navigation suite scaffold
diff --git a/compose/material3/material3-adaptive-navigation-suite/src/desktopMain/kotlin/androidx/compose/material3/adaptive/navigation-suite/NavigationSuiteScaffold.desktop.kt b/compose/material3/material3-adaptive-navigation-suite/src/desktopMain/kotlin/androidx/compose/material3/adaptive/navigationsuite/NavigationSuiteScaffold.desktop.kt
similarity index 95%
rename from compose/material3/material3-adaptive-navigation-suite/src/desktopMain/kotlin/androidx/compose/material3/adaptive/navigation-suite/NavigationSuiteScaffold.desktop.kt
rename to compose/material3/material3-adaptive-navigation-suite/src/desktopMain/kotlin/androidx/compose/material3/adaptive/navigationsuite/NavigationSuiteScaffold.desktop.kt
index 913dc42..e068dd4 100644
--- a/compose/material3/material3-adaptive-navigation-suite/src/desktopMain/kotlin/androidx/compose/material3/adaptive/navigation-suite/NavigationSuiteScaffold.desktop.kt
+++ b/compose/material3/material3-adaptive-navigation-suite/src/desktopMain/kotlin/androidx/compose/material3/adaptive/navigationsuite/NavigationSuiteScaffold.desktop.kt
@@ -14,7 +14,7 @@
  * limitations under the License.
  */
 
-package androidx.compose.material3.adaptive.navigation.suite
+package androidx.compose.material3.adaptive.navigationsuite
 
 import androidx.compose.material3.adaptive.ExperimentalMaterial3AdaptiveApi
 import androidx.compose.material3.adaptive.Posture
diff --git a/compose/material3/material3-adaptive/build.gradle b/compose/material3/material3-adaptive/build.gradle
index 2766f8b..95f48e9 100644
--- a/compose/material3/material3-adaptive/build.gradle
+++ b/compose/material3/material3-adaptive/build.gradle
@@ -42,10 +42,10 @@
         commonMain {
             dependencies {
                 implementation(libs.kotlinStdlibCommon)
-                api(project(":compose:foundation:foundation"))
-                api(project(":compose:foundation:foundation-layout"))
-                implementation(project(":compose:material3:material3-window-size-class"))
-                implementation(project(":compose:ui:ui-util"))
+                api("androidx.compose.foundation:foundation:1.6.0-rc01")
+                implementation("androidx.compose.foundation:foundation-layout:1.6.0-rc01")
+                implementation("androidx.compose.material3:material3-window-size-class:1.2.0-rc01")
+                implementation("androidx.compose.ui:ui-util:1.6.0-rc01")
             }
         }
 
diff --git a/compose/material3/material3-adaptive/samples/build.gradle b/compose/material3/material3-adaptive/samples/build.gradle
index 110964e..1c8ac3c 100644
--- a/compose/material3/material3-adaptive/samples/build.gradle
+++ b/compose/material3/material3-adaptive/samples/build.gradle
@@ -36,12 +36,12 @@
 
     compileOnly(project(":annotation:annotation-sampled"))
 
-    implementation(project(":compose:foundation:foundation"))
-    implementation(project(":compose:foundation:foundation-layout"))
+    implementation("androidx.compose.foundation:foundation:1.6.0-rc01")
+    implementation("androidx.compose.foundation:foundation-layout:1.6.0-rc01")
     implementation(project(":compose:material3:material3"))
     implementation(project(":compose:material3:material3-adaptive"))
     implementation(project(":compose:material3:material3-window-size-class"))
-    implementation(project(":compose:ui:ui-util"))
+    implementation("androidx.compose.ui:ui-util:1.6.0-rc01")
     implementation("androidx.compose.ui:ui-tooling-preview:1.4.1")
 
     debugImplementation("androidx.compose.ui:ui-tooling:1.4.1")
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 13c0af1..3ddad4c 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
@@ -21,8 +21,8 @@
 import androidx.compose.foundation.layout.Box
 import androidx.compose.foundation.layout.width
 import androidx.compose.foundation.layout.wrapContentWidth
-import androidx.compose.material3.adaptive.navigation.suite.samples.NavigationSuiteScaffoldCustomConfigSample
-import androidx.compose.material3.adaptive.navigation.suite.samples.NavigationSuiteScaffoldSample
+import androidx.compose.material3.adaptive.navigationsuite.samples.NavigationSuiteScaffoldCustomConfigSample
+import androidx.compose.material3.adaptive.navigationsuite.samples.NavigationSuiteScaffoldSample
 import androidx.compose.material3.adaptive.samples.ListDetailPaneScaffoldSample
 import androidx.compose.material3.adaptive.samples.ListDetailPaneScaffoldSampleWithExtraPane
 import androidx.compose.material3.catalog.library.util.AdaptiveNavigationSuiteSampleSourceUrl
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
index bbbed64..da46a8a 100644
--- a/compose/material3/material3/src/androidInstrumentedTest/kotlin/androidx/compose/material3/ExposedDropdownMenuTest.kt
+++ b/compose/material3/material3/src/androidInstrumentedTest/kotlin/androidx/compose/material3/ExposedDropdownMenuTest.kt
@@ -62,7 +62,10 @@
 import androidx.compose.ui.test.performTouchInput
 import androidx.compose.ui.test.swipe
 import androidx.compose.ui.unit.Density
+import androidx.compose.ui.unit.IntOffset
+import androidx.compose.ui.unit.IntRect
 import androidx.compose.ui.unit.IntSize
+import androidx.compose.ui.unit.LayoutDirection
 import androidx.compose.ui.unit.dp
 import androidx.compose.ui.viewinterop.AndroidView
 import androidx.test.ext.junit.runners.AndroidJUnit4
@@ -579,6 +582,78 @@
             .assert(SemanticsMatcher.expectValue(SemanticsProperties.Role, Role.DropdownList))
     }
 
+    @Test
+    fun edm_positionProvider() {
+        val topWindowInsets = 50
+        val density = Density(1f)
+        val anchorSize = IntSize(width = 200, height = 100)
+        val popupSize = IntSize(width = 200, height = 340)
+        val windowSize = IntSize(width = 500, height = 500)
+        val verticalMargin = with(density) { MenuVerticalMargin.roundToPx() }
+        val layoutDirection = LayoutDirection.Ltr
+
+        val edmPositionProvider = ExposedDropdownMenuPositionProvider(
+            density = density,
+            topWindowInsets = topWindowInsets,
+        )
+
+        // typical case
+        assertThat(
+            edmPositionProvider.calculatePosition(
+                anchorBounds = IntRect(
+                    size = anchorSize,
+                    offset = IntOffset(0, 0),
+                ),
+                windowSize = windowSize,
+                popupContentSize = popupSize,
+                layoutDirection = layoutDirection,
+            )
+        ).isEqualTo(IntOffset(0, anchorSize.height))
+
+        // off-screen (above)
+        assertThat(
+            edmPositionProvider.calculatePosition(
+                anchorBounds = IntRect(
+                    size = anchorSize,
+                    offset = IntOffset(0, -150),
+                ),
+                windowSize = windowSize,
+                popupContentSize = popupSize,
+                layoutDirection = layoutDirection,
+            )
+        ).isEqualTo(IntOffset(0, verticalMargin))
+
+        // interacting with window insets
+        assertThat(
+            edmPositionProvider.calculatePosition(
+                anchorBounds = IntRect(
+                    size = anchorSize,
+                    // If it weren't for topWindowInsets allowance,
+                    // the menu would be considered "off-screen"
+                    offset = IntOffset(0, 100),
+                ),
+                windowSize = windowSize,
+                popupContentSize = popupSize,
+                layoutDirection = layoutDirection,
+            )
+        ).isEqualTo(IntOffset(0, 100 + anchorSize.height))
+
+        // off-screen (below)
+        assertThat(
+            edmPositionProvider.calculatePosition(
+                anchorBounds = IntRect(
+                    size = anchorSize,
+                    offset = IntOffset(0, windowSize.height + 100),
+                ),
+                windowSize = windowSize,
+                popupContentSize = popupSize,
+                layoutDirection = layoutDirection,
+            )
+        ).isEqualTo(
+            IntOffset(0, windowSize.height + topWindowInsets - verticalMargin - popupSize.height)
+        )
+    }
+
     @Composable
     fun ExposedDropdownMenuForTest(
         expanded: Boolean,
diff --git a/compose/material3/material3/src/androidMain/kotlin/androidx/compose/material3/ExposedDropdownMenu.android.kt b/compose/material3/material3/src/androidMain/kotlin/androidx/compose/material3/ExposedDropdownMenu.android.kt
index cb83585..ff4fa9e 100644
--- a/compose/material3/material3/src/androidMain/kotlin/androidx/compose/material3/ExposedDropdownMenu.android.kt
+++ b/compose/material3/material3/src/androidMain/kotlin/androidx/compose/material3/ExposedDropdownMenu.android.kt
@@ -27,6 +27,8 @@
 import androidx.compose.foundation.layout.Box
 import androidx.compose.foundation.layout.ColumnScope
 import androidx.compose.foundation.layout.PaddingValues
+import androidx.compose.foundation.layout.WindowInsets
+import androidx.compose.foundation.layout.statusBars
 import androidx.compose.foundation.rememberScrollState
 import androidx.compose.foundation.text.selection.LocalTextSelectionColors
 import androidx.compose.foundation.text.selection.TextSelectionColors
@@ -37,6 +39,7 @@
 import androidx.compose.material3.tokens.OutlinedAutocompleteTokens
 import androidx.compose.runtime.Composable
 import androidx.compose.runtime.DisposableEffect
+import androidx.compose.runtime.Immutable
 import androidx.compose.runtime.SideEffect
 import androidx.compose.runtime.getValue
 import androidx.compose.runtime.mutableIntStateOf
@@ -66,11 +69,15 @@
 import androidx.compose.ui.semantics.semantics
 import androidx.compose.ui.semantics.stateDescription
 import androidx.compose.ui.unit.Density
-import androidx.compose.ui.unit.DpOffset
+import androidx.compose.ui.unit.IntOffset
+import androidx.compose.ui.unit.IntRect
+import androidx.compose.ui.unit.IntSize
+import androidx.compose.ui.unit.LayoutDirection
 import androidx.compose.ui.unit.constrainHeight
 import androidx.compose.ui.unit.constrainWidth
 import androidx.compose.ui.unit.dp
 import androidx.compose.ui.unit.toSize
+import androidx.compose.ui.window.PopupPositionProvider
 import kotlin.math.max
 import kotlin.math.roundToInt
 
@@ -282,10 +289,11 @@
         if (expandedState.currentState || expandedState.targetState) {
             val transformOriginState = remember { mutableStateOf(TransformOrigin.Center) }
             val density = LocalDensity.current
-            val popupPositionProvider = remember(density) {
-                DropdownMenuPositionProvider(
-                    DpOffset.Zero,
-                    density,
+            val topWindowInsets = WindowInsets.statusBars.getTop(density)
+            val popupPositionProvider = remember(density, topWindowInsets) {
+                ExposedDropdownMenuPositionProvider(
+                    density = density,
+                    topWindowInsets = topWindowInsets,
                 ) { anchorBounds, menuBounds ->
                     transformOriginState.value = calculateTransformOrigin(anchorBounds, menuBounds)
                 }
@@ -1037,6 +1045,91 @@
     )
 }
 
+@Immutable
+internal data class ExposedDropdownMenuPositionProvider(
+    val density: Density,
+    val topWindowInsets: Int,
+    val verticalMargin: Int = with(density) { MenuVerticalMargin.roundToPx() },
+    val onPositionCalculated: (anchorBounds: IntRect, menuBounds: IntRect) -> Unit = { _, _ -> }
+) : PopupPositionProvider {
+    // Horizontal position
+    private val startToAnchorStart = MenuPosition.startToAnchorStart()
+    private val endToAnchorEnd = MenuPosition.endToAnchorEnd()
+    private val leftToWindowLeft = MenuPosition.leftToWindowLeft()
+    private val rightToWindowRight = MenuPosition.rightToWindowRight()
+
+    // Vertical position
+    private val topToAnchorBottom = MenuPosition.topToAnchorBottom()
+    private val bottomToAnchorTop = MenuPosition.bottomToAnchorTop()
+    private val topToWindowTop = MenuPosition.topToWindowTop(margin = verticalMargin)
+    private val bottomToWindowBottom = MenuPosition.bottomToWindowBottom(margin = verticalMargin)
+
+    override fun calculatePosition(
+        anchorBounds: IntRect,
+        windowSize: IntSize,
+        layoutDirection: LayoutDirection,
+        popupContentSize: IntSize
+    ): IntOffset {
+        // TODO(b/256233441): Popup fails to account for window insets so we do it here instead
+        @Suppress("NAME_SHADOWING")
+        val windowSize = IntSize(windowSize.width, windowSize.height + topWindowInsets)
+
+        val xCandidates = listOf(
+            startToAnchorStart,
+            endToAnchorEnd,
+            if (anchorBounds.center.x < windowSize.width / 2) {
+                leftToWindowLeft
+            } else {
+                rightToWindowRight
+            }
+        )
+        var x = 0
+        for (index in xCandidates.indices) {
+            val xCandidate = xCandidates[index].position(
+                anchorBounds = anchorBounds,
+                windowSize = windowSize,
+                menuWidth = popupContentSize.width,
+                layoutDirection = layoutDirection
+            )
+            if (index == xCandidates.lastIndex ||
+                (xCandidate >= 0 && xCandidate + popupContentSize.width <= windowSize.width)) {
+                x = xCandidate
+                break
+            }
+        }
+
+        val yCandidates = listOf(
+            topToAnchorBottom,
+            bottomToAnchorTop,
+            if (anchorBounds.center.y < windowSize.height / 2) {
+                topToWindowTop
+            } else {
+                bottomToWindowBottom
+            }
+        )
+        var y = 0
+        for (index in yCandidates.indices) {
+            val yCandidate = yCandidates[index].position(
+                anchorBounds = anchorBounds,
+                windowSize = windowSize,
+                menuHeight = popupContentSize.height
+            )
+            if (index == yCandidates.lastIndex ||
+                (yCandidate >= 0 && yCandidate + popupContentSize.height <= windowSize.height)) {
+                y = yCandidate
+                break
+            }
+        }
+
+        val menuOffset = IntOffset(x, y)
+        onPositionCalculated(
+            /* anchorBounds = */anchorBounds,
+            /* menuBounds = */IntRect(offset = menuOffset, size = popupContentSize)
+        )
+        return menuOffset
+    }
+}
+
 private fun Modifier.expandable(
     expanded: Boolean,
     onExpandedChange: () -> Unit,
diff --git a/compose/material3/material3/src/commonMain/kotlin/androidx/compose/material3/Menu.kt b/compose/material3/material3/src/commonMain/kotlin/androidx/compose/material3/Menu.kt
index 4a9810e..5826971 100644
--- a/compose/material3/material3/src/commonMain/kotlin/androidx/compose/material3/Menu.kt
+++ b/compose/material3/material3/src/commonMain/kotlin/androidx/compose/material3/Menu.kt
@@ -118,7 +118,8 @@
                 textColor = fromToken(MenuTokens.ListItemLabelTextColor),
                 leadingIconColor = fromToken(MenuTokens.ListItemLeadingIconColor),
                 trailingIconColor = fromToken(MenuTokens.ListItemTrailingIconColor),
-                disabledTextColor = fromToken(MenuTokens.ListItemDisabledLabelTextColor),
+                disabledTextColor = fromToken(MenuTokens.ListItemDisabledLabelTextColor)
+                    .copy(alpha = MenuTokens.ListItemDisabledLabelTextOpacity),
                 disabledLeadingIconColor = fromToken(MenuTokens.ListItemDisabledLeadingIconColor)
                     .copy(alpha = MenuTokens.ListItemDisabledLeadingIconOpacity),
                 disabledTrailingIconColor = fromToken(MenuTokens.ListItemDisabledTrailingIconColor)
diff --git a/compose/material3/material3/src/commonMain/kotlin/androidx/compose/material3/MenuPosition.kt b/compose/material3/material3/src/commonMain/kotlin/androidx/compose/material3/MenuPosition.kt
index 5853b39..476b703 100644
--- a/compose/material3/material3/src/commonMain/kotlin/androidx/compose/material3/MenuPosition.kt
+++ b/compose/material3/material3/src/commonMain/kotlin/androidx/compose/material3/MenuPosition.kt
@@ -16,6 +16,8 @@
 
 package androidx.compose.material3
 
+import androidx.compose.material3.MenuPosition.Horizontal
+import androidx.compose.material3.MenuPosition.Vertical
 import androidx.compose.runtime.Immutable
 import androidx.compose.runtime.Stable
 import androidx.compose.ui.AbsoluteAlignment
@@ -26,8 +28,6 @@
 import androidx.compose.ui.unit.IntRect
 import androidx.compose.ui.unit.IntSize
 import androidx.compose.ui.unit.LayoutDirection
-import androidx.compose.ui.util.fastFirstOrNull
-import androidx.compose.ui.util.fastMap
 import androidx.compose.ui.window.PopupPositionProvider
 
 /**
@@ -368,17 +368,21 @@
             } else {
                 rightToWindowRight
             }
-        ).fastMap {
-            it.position(
+        )
+        var x = 0
+        for (index in xCandidates.indices) {
+            val xCandidate = xCandidates[index].position(
                 anchorBounds = anchorBounds,
                 windowSize = windowSize,
                 menuWidth = popupContentSize.width,
                 layoutDirection = layoutDirection
             )
+            if (index == xCandidates.lastIndex ||
+                (xCandidate >= 0 && xCandidate + popupContentSize.width <= windowSize.width)) {
+                x = xCandidate
+                break
+            }
         }
-        val x = xCandidates.fastFirstOrNull {
-            it >= 0 && it + popupContentSize.width <= windowSize.width
-        } ?: xCandidates.last()
 
         val yCandidates = listOf(
             topToAnchorBottom,
@@ -389,17 +393,21 @@
             } else {
                 bottomToWindowBottom
             }
-        ).fastMap {
-            it.position(
+        )
+        var y = 0
+        for (index in yCandidates.indices) {
+            val yCandidate = yCandidates[index].position(
                 anchorBounds = anchorBounds,
                 windowSize = windowSize,
                 menuHeight = popupContentSize.height
             )
+            if (index == yCandidates.lastIndex ||
+                (yCandidate >= verticalMargin &&
+                    yCandidate + popupContentSize.height <= windowSize.height - verticalMargin)) {
+                y = yCandidate
+                break
+            }
         }
-        val y = yCandidates.fastFirstOrNull {
-            it >= verticalMargin &&
-                it + popupContentSize.height <= windowSize.height - verticalMargin
-        } ?: yCandidates.last()
 
         val menuOffset = IntOffset(x, y)
         onPositionCalculated(
diff --git a/compose/material3/material3/src/commonMain/kotlin/androidx/compose/material3/carousel/Carousel.kt b/compose/material3/material3/src/commonMain/kotlin/androidx/compose/material3/carousel/Carousel.kt
index dd2f455..f27a6b6 100644
--- a/compose/material3/material3/src/commonMain/kotlin/androidx/compose/material3/carousel/Carousel.kt
+++ b/compose/material3/material3/src/commonMain/kotlin/androidx/compose/material3/carousel/Carousel.kt
@@ -102,7 +102,7 @@
     val interpolatedKeyline = lerp(keylineBefore, keylineAfter, progress)
     val isOutOfKeylineBounds = keylineBefore == keylineAfter
 
-    return this then layout { measurable, constraints ->
+    return layout { measurable, constraints ->
         // Force the item to use the strategy's itemMainAxisSize along its main axis
         val mainAxisSize = strategy.itemMainAxisSize
         val itemConstraints = if (isVertical) {
@@ -125,7 +125,7 @@
         layout(placeable.width, placeable.height) {
             placeable.place(0, 0)
         }
-    } then graphicsLayer {
+    }.graphicsLayer {
         // Clip the item
         clip = true
         shape = object : Shape {
diff --git a/compose/material3/material3/src/commonMain/kotlin/androidx/compose/material3/compose-material-3-documentation.md b/compose/material3/material3/src/commonMain/kotlin/androidx/compose/material3/compose-material-3-documentation.md
index 4600350..177e18c 100644
--- a/compose/material3/material3/src/commonMain/kotlin/androidx/compose/material3/compose-material-3-documentation.md
+++ b/compose/material3/material3/src/commonMain/kotlin/androidx/compose/material3/compose-material-3-documentation.md
@@ -57,7 +57,8 @@
 |                         | [DatePickerDialog]             | M3 date picker embeeded in dialog         |
 |                         | [DateRangePicker]              | M3 date range picker                      |
 | **Dialogs**             | [AlertDialog]                  | M3 basic dialog                           |
-| **Dividers**            | [Divider]                      | M3 divider                                |
+| **Dividers**            | [HorizontalDivider]            | M3 horizontal divider                     |
+|                         | [VerticalDivider]              | M3 vertical divider                       |
 | **Extended FAB**        | [ExtendedFloatingActionButton] | M3 extended FAB                           |
 | **FAB**                 | [FloatingActionButton]         | M3 FAB                                    |
 |                         | [SmallFloatingActionButton]    | M3 small FAB                              |
@@ -87,6 +88,7 @@
 |                         | [NavigationRailItem]           | M3 navigation rail item                   |
 | **Progress indicators** | [LinearProgressIndicator]      | M3 linear progress indicator              |
 |                         | [CircularProgressIndicator]    | M3 circular progress indicator            |
+| **Pull refresh **       | [PullToRefreshContainer]       | M3 pull to refresh indicator              |
 | **Radio button**        | [RadioButton]                  | M3 radio button                           |
 | **Search Bar**          | [SearchBar]                    | M3 search bar                             |
 |                         | [DockedSearchBar]              | M3 docked search bar                      |
diff --git a/compose/runtime/runtime-lint/src/main/java/androidx/compose/runtime/lint/AutoboxingStateCreationDetector.kt b/compose/runtime/runtime-lint/src/main/java/androidx/compose/runtime/lint/AutoboxingStateCreationDetector.kt
index b3763a5..aeb86fe 100644
--- a/compose/runtime/runtime-lint/src/main/java/androidx/compose/runtime/lint/AutoboxingStateCreationDetector.kt
+++ b/compose/runtime/runtime-lint/src/main/java/androidx/compose/runtime/lint/AutoboxingStateCreationDetector.kt
@@ -40,7 +40,7 @@
 import org.jetbrains.kotlin.psi.KtValueArgumentList
 import org.jetbrains.kotlin.utils.addToStdlib.firstIsInstanceOrNull
 import org.jetbrains.uast.UCallExpression
-import org.jetbrains.uast.java.JavaUCallExpression
+import org.jetbrains.uast.kotlin.isKotlin
 import org.jetbrains.uast.skipParenthesizedExprDown
 
 /**
@@ -71,7 +71,7 @@
     override fun getApplicableMethodNames() = listOf(Names.Runtime.MutableStateOf.shortName)
 
     override fun visitMethodCall(context: JavaContext, node: UCallExpression, method: PsiMethod) {
-        if (node is JavaUCallExpression) return
+        if (!isKotlin(node.lang)) return
         if (!method.isInPackageName(Names.Runtime.PackageName)) return
 
         val replacement = getSuggestedReplacementName(node) ?: return
diff --git a/compose/runtime/runtime/src/commonMain/kotlin/androidx/compose/runtime/Applier.kt b/compose/runtime/runtime/src/commonMain/kotlin/androidx/compose/runtime/Applier.kt
index 762d8f1..694aff4 100644
--- a/compose/runtime/runtime/src/commonMain/kotlin/androidx/compose/runtime/Applier.kt
+++ b/compose/runtime/runtime/src/commonMain/kotlin/androidx/compose/runtime/Applier.kt
@@ -203,7 +203,7 @@
     }
 
     override fun up() {
-        check(stack.isNotEmpty()) { "empty stack" }
+        checkPrecondition(stack.isNotEmpty()) { "empty stack" }
         current = stack.removeAt(stack.size - 1)
     }
 
diff --git a/compose/runtime/runtime/src/commonMain/kotlin/androidx/compose/runtime/ComposeVersion.kt b/compose/runtime/runtime/src/commonMain/kotlin/androidx/compose/runtime/ComposeVersion.kt
index 283b6e3..ee4f5ad 100644
--- a/compose/runtime/runtime/src/commonMain/kotlin/androidx/compose/runtime/ComposeVersion.kt
+++ b/compose/runtime/runtime/src/commonMain/kotlin/androidx/compose/runtime/ComposeVersion.kt
@@ -28,5 +28,5 @@
      * IMPORTANT: Whenever updating this value, please make sure to also update `versionTable` and
      * `minimumRuntimeVersionInt` in `VersionChecker.kt` of the compiler.
      */
-    const val version: Int = 12000
+    const val version: Int = 12100
 }
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 69d2ce2..83aaba4 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
@@ -35,6 +35,8 @@
 import androidx.compose.runtime.snapshots.fastToSet
 import androidx.compose.runtime.tooling.CompositionData
 import androidx.compose.runtime.tooling.LocalInspectionTables
+import kotlin.contracts.ExperimentalContracts
+import kotlin.contracts.contract
 import kotlin.coroutines.CoroutineContext
 
 private class GroupInfo(
@@ -100,7 +102,7 @@
     var groupIndex: Int = 0
 
     init {
-        require(startIndex >= 0) { "Invalid start index" }
+        requirePrecondition(startIndex >= 0) { "Invalid start index" }
     }
 
     private val usedKeys = mutableListOf<KeyInfo>()
@@ -1703,7 +1705,7 @@
     }
 
     fun endReuseFromRoot() {
-        require(!isComposing && reusingGroup == rootKey) {
+        requirePrecondition(!isComposing && reusingGroup == rootKey) {
             "Cannot disable reuse from root if it was caused by other groups"
         }
         reusingGroup = -1
@@ -4313,10 +4315,14 @@
 
 internal class ComposeRuntimeError(override val message: String) : IllegalStateException()
 
-internal inline fun runtimeCheck(value: Boolean, lazyMessage: () -> Any) {
+@Suppress("BanInlineOptIn")
+@OptIn(ExperimentalContracts::class)
+internal inline fun runtimeCheck(value: Boolean, lazyMessage: () -> String) {
+    contract {
+        returns() implies value
+    }
     if (!value) {
-        val message = lazyMessage()
-        composeRuntimeError(message.toString())
+        composeImmediateRuntimeError(lazyMessage())
     }
 }
 
@@ -4330,6 +4336,16 @@
     )
 }
 
+// Unit variant of composeRuntimeError() so the call site doesn't add 3 extra
+// instructions to throw a KotlinNothingValueException
+internal fun composeImmediateRuntimeError(message: String) {
+    throw ComposeRuntimeError(
+        "Compose Runtime internal error. Unexpected or incorrect use of the Compose " +
+            "internal runtime API ($message). Please report to Google or use " +
+            "https://goo.gle/compose-feedback"
+    )
+}
+
 private val InvalidationLocationAscending = Comparator<Invalidation> { i1, i2 ->
     i1.location.compareTo(i2.location)
 }
diff --git a/compose/runtime/runtime/src/commonMain/kotlin/androidx/compose/runtime/Composition.kt b/compose/runtime/runtime/src/commonMain/kotlin/androidx/compose/runtime/Composition.kt
index a7a6180..4bac17f 100644
--- a/compose/runtime/runtime/src/commonMain/kotlin/androidx/compose/runtime/Composition.kt
+++ b/compose/runtime/runtime/src/commonMain/kotlin/androidx/compose/runtime/Composition.kt
@@ -639,7 +639,7 @@
     }
 
     private fun composeInitial(content: @Composable () -> Unit) {
-        check(!disposed) { "The composition is disposed" }
+        checkPrecondition(!disposed) { "The composition is disposed" }
         this.composable = content
         parent.composeInitial(this, composable)
     }
@@ -742,7 +742,7 @@
 
     override fun dispose() {
         synchronized(lock) {
-            check(!composer.isComposing) {
+            checkPrecondition(!composer.isComposing) {
                 "Composition is disposed while composing. If dispose is triggered by a call in " +
                     "@Composable function, consider wrapping it with SideEffect block."
             }
@@ -1206,7 +1206,7 @@
         val scopes = slotTable.slots.mapNotNull { it as? RecomposeScopeImpl }
         scopes.fastForEach { scope ->
             scope.anchor?.let { anchor ->
-                check(scope in slotTable.slotsOf(anchor.toIndexFor(slotTable))) {
+                checkPrecondition(scope in slotTable.slotsOf(anchor.toIndexFor(slotTable))) {
                     val dataIndex = slotTable.slots.indexOf(scope)
                     "Misaligned anchor $anchor in scope $scope encountered, scope found at " +
                         "$dataIndex"
diff --git a/compose/runtime/runtime/src/commonMain/kotlin/androidx/compose/runtime/Preconditions.kt b/compose/runtime/runtime/src/commonMain/kotlin/androidx/compose/runtime/Preconditions.kt
new file mode 100644
index 0000000..90aa989
--- /dev/null
+++ b/compose/runtime/runtime/src/commonMain/kotlin/androidx/compose/runtime/Preconditions.kt
@@ -0,0 +1,67 @@
+/*
+ * Copyright 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.compose.runtime
+
+import kotlin.contracts.ExperimentalContracts
+import kotlin.contracts.contract
+
+// This function exists so we do *not* inline the throw. It keeps
+// the call site much smaller and since it's the slow path anyway,
+// we don't mind the extra function call
+internal fun throwIllegalArgumentException(message: String) {
+    throw IllegalArgumentException(message)
+}
+
+// Like Kotlin's require() but without the .toString() call
+@Suppress("BanInlineOptIn") // same opt-in as using Kotlin's require()
+@OptIn(ExperimentalContracts::class)
+internal inline fun requirePrecondition(value: Boolean, lazyMessage: () -> String) {
+    contract {
+        returns() implies value
+    }
+    if (!value) {
+        throwIllegalArgumentException(lazyMessage())
+    }
+}
+
+// See above
+internal fun throwIllegalStateException(message: String) {
+    throw IllegalStateException(message)
+}
+
+// Like Kotlin's check() but without the .toString() call
+@Suppress("BanInlineOptIn") // same opt-in as using Kotlin's check()
+@OptIn(ExperimentalContracts::class)
+internal inline fun checkPrecondition(value: Boolean, lazyMessage: () -> String) {
+    contract {
+        returns() implies value
+    }
+    if (!value) {
+        throwIllegalStateException(lazyMessage())
+    }
+}
+
+@Suppress("BanInlineOptIn", "NOTHING_TO_INLINE")
+@OptIn(ExperimentalContracts::class)
+internal inline fun checkPrecondition(value: Boolean) {
+    contract {
+        returns() implies value
+    }
+    if (!value) {
+        throwIllegalStateException("Check failed.")
+    }
+}
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 144b1fb..910a9a1 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
@@ -852,7 +852,7 @@
     suspend fun runRecomposeConcurrentlyAndApplyChanges(
         recomposeCoroutineContext: CoroutineContext
     ) = recompositionRunner { parentFrameClock ->
-        require(recomposeCoroutineContext[Job] == null) {
+        requirePrecondition(recomposeCoroutineContext[Job] == null) {
             "recomposeCoroutineContext may not contain a Job; found " +
                 recomposeCoroutineContext[Job]
         }
@@ -1559,7 +1559,7 @@
      * available up until this point. (Synchronizing access to that data is up to the caller.)
      */
     fun takeFrameRequestLocked() {
-        check(pendingFrameContinuation === FramePending) { "frame not pending" }
+        checkPrecondition(pendingFrameContinuation === FramePending) { "frame not pending" }
         pendingFrameContinuation = null
     }
 
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 b207499..2177e18 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
@@ -218,7 +218,9 @@
      */
     fun anchor(index: Int): Anchor {
         runtimeCheck(!writer) { "use active SlotWriter to create an anchor location instead" }
-        require(index in 0 until groupsSize) { "Parameter index is out of range" }
+        requirePrecondition(index in 0 until groupsSize) {
+            "Parameter index is out of range"
+        }
         return anchors.getOrAdd(index, groupsSize) {
             Anchor(index)
         }
@@ -240,7 +242,7 @@
      */
     fun anchorIndex(anchor: Anchor): Int {
         runtimeCheck(!writer) { "Use active SlotWriter to determine anchor location instead" }
-        require(anchor.valid) { "Anchor refers to a group that was removed" }
+        requirePrecondition(anchor.valid) { "Anchor refers to a group that was removed" }
         return anchor.location
     }
 
@@ -301,7 +303,7 @@
         sourceInformationMap: HashMap<Anchor, GroupSourceInformation>?,
         calledByMap: MutableIntObjectMap<MutableIntSet>?
     ) {
-        require(writer.table === this && this.writer) { "Unexpected writer close()" }
+        requirePrecondition(writer.table === this && this.writer) { "Unexpected writer close()" }
         this.writer = false
         setTo(groups, groupsSize, slots, slotsSize, anchors, sourceInformationMap, calledByMap)
     }
@@ -438,38 +440,38 @@
         fun validateGroup(parent: Int, parentEnd: Int): Int {
             val group = current++
             val parentIndex = groups.parentAnchor(group)
-            check(parentIndex == parent) {
+            checkPrecondition(parentIndex == parent) {
                 "Invalid parent index detected at $group, expected parent index to be $parent " +
                     "found $parentIndex"
             }
             val end = group + groups.groupSize(group)
-            check(end <= groupsSize) {
+            checkPrecondition(end <= groupsSize) {
                 "A group extends past the end of the table at $group"
             }
-            check(end <= parentEnd) {
+            checkPrecondition(end <= parentEnd) {
                 "A group extends past its parent group at $group"
             }
 
             val dataStart = groups.dataAnchor(group)
             val dataEnd = if (group >= groupsSize - 1) slotsSize else groups.dataAnchor(group + 1)
-            check(dataEnd <= slots.size) {
+            checkPrecondition(dataEnd <= slots.size) {
                 "Slots for $group extend past the end of the slot table"
             }
-            check(dataStart <= dataEnd) {
+            checkPrecondition(dataStart <= dataEnd) {
                 "Invalid data anchor at $group"
             }
             val slotStart = groups.slotAnchor(group)
-            check(slotStart <= dataEnd) {
+            checkPrecondition(slotStart <= dataEnd) {
                 "Slots start out of range at $group"
             }
             val minSlotsNeeded = (if (groups.isNode(group)) 1 else 0) +
                 (if (groups.hasObjectKey(group)) 1 else 0) +
                 (if (groups.hasAux(group)) 1 else 0)
-            check(dataEnd - dataStart >= minSlotsNeeded) {
+            checkPrecondition(dataEnd - dataStart >= minSlotsNeeded) {
                 "Not enough slots added for group $group"
             }
             val isNode = groups.isNode(group)
-            check(!isNode || slots[groups.nodeIndex(group)] != null) {
+            checkPrecondition(!isNode || slots[groups.nodeIndex(group)] != null) {
                 "No node recorded for a node group at $group"
             }
             var nodeCount = 0
@@ -478,17 +480,17 @@
             }
             val expectedNodeCount = groups.nodeCount(group)
             val expectedSlotCount = groups.groupSize(group)
-            check(expectedNodeCount == nodeCount) {
+            checkPrecondition(expectedNodeCount == nodeCount) {
                 "Incorrect node count detected at $group, " +
                     "expected $expectedNodeCount, received $nodeCount"
             }
             val actualSlotCount = current - group
-            check(expectedSlotCount == actualSlotCount) {
+            checkPrecondition(expectedSlotCount == actualSlotCount) {
                 "Incorrect slot count detected at $group, expected $expectedSlotCount, received " +
                     "$actualSlotCount"
             }
             if (groups.containsAnyMark(group)) {
-                check(group <= 0 || groups.containsMark(parent)) {
+                checkPrecondition(group <= 0 || groups.containsMark(parent)) {
                     "Expected group $parent to record it contains a mark because $group does"
                 }
             }
@@ -500,14 +502,14 @@
             while (current < groupsSize) {
                 validateGroup(-1, current + groups.groupSize(current))
             }
-            check(current == groupsSize) {
+            checkPrecondition(current == groupsSize) {
                 "Incomplete group at root $current expected to be $groupsSize"
             }
         }
 
         // Verify that slot gap contains all nulls
         for (index in slotsSize until slots.size) {
-            check(slots[index] == null) {
+            checkPrecondition(slots[index] == null) {
                 "Non null value in the slot gap at index $index"
             }
         }
@@ -516,8 +518,10 @@
         var lastLocation = -1
         anchors.fastForEach { anchor ->
             val location = anchor.toIndexFor(this)
-            require(location in 0..groupsSize) { "Invalid anchor, location out of bound" }
-            require(lastLocation < location) { "Anchor is out of order" }
+            requirePrecondition(location in 0..groupsSize) {
+                "Invalid anchor, location out of bound"
+            }
+            requirePrecondition(lastLocation < location) { "Anchor is out of order" }
             lastLocation = location
         }
 
@@ -526,10 +530,10 @@
             group.groups?.fastForEach { item ->
                 when (item) {
                     is Anchor -> {
-                        require(item.valid) {
+                        requirePrecondition(item.valid) {
                             "Source map contains invalid anchor"
                         }
-                        require(ownsAnchor(item)) {
+                        requirePrecondition(ownsAnchor(item)) {
                             "Source map anchor is not owned by the slot table"
                         }
                     }
@@ -540,10 +544,10 @@
 
         sourceInformationMap?.let { sourceInformationMap ->
             for ((anchor, sourceGroup) in sourceInformationMap) {
-                require(anchor.valid) {
+                requirePrecondition(anchor.valid) {
                     "Source map contains invalid anchor"
                 }
-                require(ownsAnchor(anchor)) {
+                requirePrecondition(ownsAnchor(anchor)) {
                     "Source map anchor is not owned by the slot table"
                 }
                 verifySourceGroup(sourceGroup)
@@ -1061,7 +1065,7 @@
      */
     fun parentOf(index: Int): Int {
         @Suppress("ConvertTwoComparisonsToRangeCheck")
-        require(index >= 0 && index < groupsSize) { "Invalid group index $index" }
+        requirePrecondition(index >= 0 && index < groupsSize) { "Invalid group index $index" }
         return groups.parentAnchor(index)
     }
 
@@ -1132,7 +1136,7 @@
      * End reporting [Composer.Empty] for calls to [next] and [get],
      */
     fun endEmpty() {
-        require(emptyCount > 0) { "Unbalanced begin/end empty" }
+        requirePrecondition(emptyCount > 0) { "Unbalanced begin/end empty" }
         emptyCount--
     }
 
@@ -1152,7 +1156,9 @@
         if (emptyCount <= 0) {
             val parent = parent
             val currentGroup = currentGroup
-            require(groups.parentAnchor(currentGroup) == parent) { "Invalid slot table detected" }
+            requirePrecondition(groups.parentAnchor(currentGroup) == parent) {
+                "Invalid slot table detected"
+            }
             sourceInformationMap?.get(anchor(parent))?.reportGroup(table, currentGroup)
             val currentSlotStack = currentSlotStack
             val currentSlot = currentSlot
@@ -1177,7 +1183,7 @@
      */
     fun startNode() {
         if (emptyCount <= 0) {
-            require(groups.isNode(currentGroup)) { "Expected a node group" }
+            requirePrecondition(groups.isNode(currentGroup)) { "Expected a node group" }
             startGroup()
         }
     }
@@ -1696,7 +1702,7 @@
             // scope inserted by a restart group and the lambda value in a composableLambda
             // instance) so this is the only case currently supported.
             val slotsToMove = currentSlot - auxIndex
-            check(slotsToMove < 3) { "Moving more than two slot not supported" }
+            checkPrecondition(slotsToMove < 3) { "Moving more than two slot not supported" }
             if (slotsToMove > 1) {
                 slots[auxAddress + 2] = slots[auxAddress + 1]
             }
@@ -1875,7 +1881,7 @@
      */
     fun advanceBy(amount: Int) {
         runtimeCheck(amount >= 0) { "Cannot seek backwards" }
-        check(insertCount <= 0) { "Cannot call seek() while inserting" }
+        checkPrecondition(insertCount <= 0) { "Cannot call seek() while inserting" }
         if (amount == 0) return
         val index = currentGroup + amount
         @Suppress("ConvertTwoComparisonsToRangeCheck")
@@ -1917,7 +1923,7 @@
      * Ends inserting.
      */
     fun endInsert() {
-        check(insertCount > 0) { "Unbalanced begin/end insert" }
+        checkPrecondition(insertCount > 0) { "Unbalanced begin/end insert" }
         if (--insertCount == 0) {
             runtimeCheck(nodeCountStack.size == startStack.size) {
                 "startGroup/endGroup mismatch while inserting"
@@ -3347,15 +3353,15 @@
             val address = groupIndexToAddress(index)
             val dataAnchor = groups.dataAnchor(address)
             val dataIndex = groups.dataIndex(address)
-            check(dataIndex >= previousDataIndex) {
+            checkPrecondition(dataIndex >= previousDataIndex) {
                 "Data index out of order at $index, previous = $previousDataIndex, current = " +
                     "$dataIndex"
             }
-            check(dataIndex <= slotsSize) {
+            checkPrecondition(dataIndex <= slotsSize) {
                 "Data index, $dataIndex, out of bound at $index"
             }
             if (dataAnchor < 0 && !ownerFound) {
-                check(owner == index) {
+                checkPrecondition(owner == index) {
                     "Expected the slot gap owner to be $owner found gap at $index"
                 }
                 ownerFound = true
@@ -3371,7 +3377,7 @@
         val capacity = capacity
         for (groupAddress in 0 until gapStart) {
             val parentAnchor = groups.parentAnchor(groupAddress)
-            check(parentAnchor > parentAnchorPivot) {
+            checkPrecondition(parentAnchor > parentAnchorPivot) {
                 "Expected a start relative anchor at $groupAddress"
             }
         }
@@ -3379,11 +3385,11 @@
             val parentAnchor = groups.parentAnchor(groupAddress)
             val parentIndex = parentAnchorToIndex(parentAnchor)
             if (parentIndex < gapStart) {
-                check(parentAnchor > parentAnchorPivot) {
+                checkPrecondition(parentAnchor > parentAnchorPivot) {
                     "Expected a start relative anchor at $groupAddress"
                 }
             } else {
-                check(parentAnchor <= parentAnchorPivot) {
+                checkPrecondition(parentAnchor <= parentAnchorPivot) {
                     "Expected an end relative anchor at $groupAddress"
                 }
             }
@@ -4111,8 +4117,8 @@
         for (index in 0 until size / 2) {
             val left = (index + 1) * 2 - 1
             val right = (index + 1) * 2
-            check(list[index] >= list[left])
-            check(right >= size || list[index] >= list[right])
+            checkPrecondition(list[index] >= list[left])
+            checkPrecondition(right >= size || list[index] >= list[right])
         }
     }
 }
diff --git a/compose/runtime/runtime/src/commonMain/kotlin/androidx/compose/runtime/changelist/Operations.kt b/compose/runtime/runtime/src/commonMain/kotlin/androidx/compose/runtime/changelist/Operations.kt
index 11956a3..46fe6b5 100644
--- a/compose/runtime/runtime/src/commonMain/kotlin/androidx/compose/runtime/changelist/Operations.kt
+++ b/compose/runtime/runtime/src/commonMain/kotlin/androidx/compose/runtime/changelist/Operations.kt
@@ -22,6 +22,8 @@
 import androidx.compose.runtime.SlotWriter
 import androidx.compose.runtime.changelist.Operation.IntParameter
 import androidx.compose.runtime.changelist.Operation.ObjectParameter
+import androidx.compose.runtime.checkPrecondition
+import androidx.compose.runtime.requirePrecondition
 import kotlin.contracts.ExperimentalContracts
 import kotlin.contracts.InvocationKind.EXACTLY_ONCE
 import kotlin.contracts.contract
@@ -141,7 +143,7 @@
      * defines any arguments.
      */
     fun push(operation: Operation) {
-        require(operation.ints == 0 && operation.objects == 0) {
+        requirePrecondition(operation.ints == 0 && operation.objects == 0) {
             "Cannot push $operation without arguments because it expects " +
                 "${operation.ints} ints and ${operation.objects} objects."
         }
@@ -168,7 +170,7 @@
         WriteScope(this).args()
 
         // Verify all arguments were written to.
-        check(
+        checkPrecondition(
             pushedIntMask == createExpectedArgMask(operation.ints) &&
                 pushedObjectMask == createExpectedArgMask(operation.objects)
         ) {
@@ -330,7 +332,7 @@
 
         fun setInt(parameter: IntParameter, value: Int) = with(stack) {
             val mask = 0b1 shl parameter.offset
-            check(pushedIntMask and mask == 0) {
+            checkPrecondition(pushedIntMask and mask == 0) {
                 "Already pushed argument ${operation.intParamName(parameter)}"
             }
             pushedIntMask = pushedIntMask or mask
@@ -339,7 +341,7 @@
 
         fun <T> setObject(parameter: ObjectParameter<T>, value: T) = with(stack) {
             val mask = 0b1 shl parameter.offset
-            check(pushedObjectMask and mask == 0) {
+            checkPrecondition(pushedObjectMask and mask == 0) {
                 "Already pushed argument ${operation.objectParamName(parameter)}"
             }
             pushedObjectMask = pushedObjectMask or mask
diff --git a/compose/runtime/runtime/src/commonMain/kotlin/androidx/compose/runtime/external/kotlinx/collections/immutable/implementations/immutableList/PersistentVector.kt b/compose/runtime/runtime/src/commonMain/kotlin/androidx/compose/runtime/external/kotlinx/collections/immutable/implementations/immutableList/PersistentVector.kt
index 1e95898..5c18dcd 100644
--- a/compose/runtime/runtime/src/commonMain/kotlin/androidx/compose/runtime/external/kotlinx/collections/immutable/implementations/immutableList/PersistentVector.kt
+++ b/compose/runtime/runtime/src/commonMain/kotlin/androidx/compose/runtime/external/kotlinx/collections/immutable/implementations/immutableList/PersistentVector.kt
@@ -9,7 +9,7 @@
 import androidx.compose.runtime.external.kotlinx.collections.immutable.internal.ListImplementation.checkElementIndex
 import androidx.compose.runtime.external.kotlinx.collections.immutable.internal.ListImplementation.checkPositionIndex
 import androidx.compose.runtime.external.kotlinx.collections.immutable.internal.assert
-import androidx.compose.runtime.external.kotlinx.collections.immutable.mutate
+import androidx.compose.runtime.requirePrecondition
 
 /**
  * Persistent vector made of a trie of leaf buffers entirely filled with [MAX_BUFFER_SIZE] elements and a tail having
@@ -27,7 +27,10 @@
                                    private val rootShift: Int) : PersistentList<E>, AbstractPersistentList<E>() {
 
     init {
-        require(size > MAX_BUFFER_SIZE) { "Trie-based persistent vector should have at least ${MAX_BUFFER_SIZE + 1} elements, got $size" }
+        requirePrecondition(size > MAX_BUFFER_SIZE) {
+            "Trie-based persistent vector should have at least " +
+                "${MAX_BUFFER_SIZE + 1} elements, got $size"
+        }
         assert(size - rootSize(size) <= tail.size.coerceAtMost(MAX_BUFFER_SIZE))
     }
 
diff --git a/compose/runtime/runtime/src/commonMain/kotlin/androidx/compose/runtime/external/kotlinx/collections/immutable/implementations/immutableList/PersistentVectorBuilder.kt b/compose/runtime/runtime/src/commonMain/kotlin/androidx/compose/runtime/external/kotlinx/collections/immutable/implementations/immutableList/PersistentVectorBuilder.kt
index ad6aa069..c2d4fd1 100644
--- a/compose/runtime/runtime/src/commonMain/kotlin/androidx/compose/runtime/external/kotlinx/collections/immutable/implementations/immutableList/PersistentVectorBuilder.kt
+++ b/compose/runtime/runtime/src/commonMain/kotlin/androidx/compose/runtime/external/kotlinx/collections/immutable/implementations/immutableList/PersistentVectorBuilder.kt
@@ -10,6 +10,7 @@
 import androidx.compose.runtime.external.kotlinx.collections.immutable.internal.ListImplementation.checkPositionIndex
 import androidx.compose.runtime.external.kotlinx.collections.immutable.internal.MutabilityOwnership
 import androidx.compose.runtime.external.kotlinx.collections.immutable.internal.assert
+import androidx.compose.runtime.requirePrecondition
 
 internal class PersistentVectorBuilder<E>(private var vector: PersistentList<E>,
                                           private var vectorRoot: Array<Any?>?,
@@ -238,8 +239,8 @@
      * Returns the resulting root.
      */
     private fun pushBuffers(root: Array<Any?>?, rootSize: Int, shift: Int, buffersIterator: Iterator<Array<Any?>>): Array<Any?> {
-        require(buffersIterator.hasNext()) {"invalid buffersIterator"}
-        require(shift >= 0) {"negative shift"}
+        requirePrecondition(buffersIterator.hasNext()) {"invalid buffersIterator"}
+        requirePrecondition(shift >= 0) {"negative shift"}
 
         if (shift == 0) {
             return buffersIterator.next()
@@ -476,7 +477,7 @@
             nullBuffers: Int,
             nextBuffer: Array<Any?>
     ) {
-        require(nullBuffers >= 1) { "requires at least one nullBuffer" }
+        requirePrecondition(nullBuffers >= 1) { "requires at least one nullBuffer" }
 
         val firstBuffer = makeMutable(startBuffer)
         buffers[0] = firstBuffer
@@ -742,7 +743,7 @@
      * If the height of the root is bigger than needed to store [size] elements, it's decreased.
      */
     private fun retainFirst(root: Array<Any?>, size: Int): Array<Any?>? {
-        require(size and MAX_BUFFER_SIZE_MINUS_ONE == 0) { "invalid size" }
+        requirePrecondition(size and MAX_BUFFER_SIZE_MINUS_ONE == 0) { "invalid size" }
 
         if (size == 0) {
             rootShift = 0
@@ -765,7 +766,7 @@
      * Used to prevent memory leaks after reusing nodes.
      */
     private fun nullifyAfter(root: Array<Any?>, index: Int, shift: Int): Array<Any?> {
-        require(shift >= 0) { "shift should be positive" }
+        requirePrecondition(shift >= 0) { "shift should be positive" }
 
         if (shift == 0) {
             // the `root` is a leaf buffer.
diff --git a/compose/runtime/runtime/src/commonMain/kotlin/androidx/compose/runtime/external/kotlinx/collections/immutable/implementations/immutableMap/TrieNode.kt b/compose/runtime/runtime/src/commonMain/kotlin/androidx/compose/runtime/external/kotlinx/collections/immutable/implementations/immutableMap/TrieNode.kt
index f97c0ed..8165c7f 100644
--- a/compose/runtime/runtime/src/commonMain/kotlin/androidx/compose/runtime/external/kotlinx/collections/immutable/implementations/immutableMap/TrieNode.kt
+++ b/compose/runtime/runtime/src/commonMain/kotlin/androidx/compose/runtime/external/kotlinx/collections/immutable/implementations/immutableMap/TrieNode.kt
@@ -9,6 +9,7 @@
 import androidx.compose.runtime.external.kotlinx.collections.immutable.internal.MutabilityOwnership
 import androidx.compose.runtime.external.kotlinx.collections.immutable.internal.assert
 import androidx.compose.runtime.external.kotlinx.collections.immutable.internal.forEachOneBit
+import androidx.compose.runtime.checkPrecondition
 
 
 internal const val MAX_BRANCHING_FACTOR = 32
@@ -636,7 +637,7 @@
             // we can use this later to skip calling equals() again
         }
         @Suppress("ExceptionMessage")
-        check(newNodeMap and newDataMap == 0)
+        checkPrecondition(newNodeMap and newDataMap == 0)
         val mutableNode = when {
             this.ownedBy == mutator.ownership && this.dataMap == newDataMap && this.nodeMap == newNodeMap -> this
             else -> {
diff --git a/compose/runtime/runtime/src/commonMain/kotlin/androidx/compose/runtime/snapshots/Snapshot.kt b/compose/runtime/runtime/src/commonMain/kotlin/androidx/compose/runtime/snapshots/Snapshot.kt
index 58759f2..d08defb 100644
--- a/compose/runtime/runtime/src/commonMain/kotlin/androidx/compose/runtime/snapshots/Snapshot.kt
+++ b/compose/runtime/runtime/src/commonMain/kotlin/androidx/compose/runtime/snapshots/Snapshot.kt
@@ -23,9 +23,11 @@
 import androidx.compose.runtime.ExperimentalComposeApi
 import androidx.compose.runtime.InternalComposeApi
 import androidx.compose.runtime.SnapshotThreadLocal
+import androidx.compose.runtime.checkPrecondition
 import androidx.compose.runtime.collection.IdentityArraySet
 import androidx.compose.runtime.currentThreadId
 import androidx.compose.runtime.internal.JvmDefaultWithCompatibility
+import androidx.compose.runtime.requirePrecondition
 import androidx.compose.runtime.snapshots.Snapshot.Companion.takeMutableSnapshot
 import androidx.compose.runtime.snapshots.Snapshot.Companion.takeSnapshot
 import androidx.compose.runtime.snapshots.SnapshotApplyResult.Failure
@@ -179,7 +181,7 @@
      */
     @ExperimentalComposeApi
     fun unsafeLeave(oldSnapshot: Snapshot?) {
-        check(threadSnapshot.get() === this) {
+        checkPrecondition(threadSnapshot.get() === this) {
             "Cannot leave snapshot; $this is not the current snapshot"
         }
         restoreCurrent(oldSnapshot)
@@ -273,7 +275,7 @@
     }
 
     internal fun validateNotDisposed() {
-        require(!disposed) { "Cannot use a disposed snapshot" }
+        requirePrecondition(!disposed) { "Cannot use a disposed snapshot" }
     }
 
     internal fun releasePinnedSnapshotLocked() {
@@ -912,7 +914,7 @@
     override fun nestedActivated(snapshot: Snapshot) { snapshots++ }
 
     override fun nestedDeactivated(snapshot: Snapshot) {
-        require(snapshots > 0) { "no pending nested snapshots" }
+        requirePrecondition(snapshots > 0) { "no pending nested snapshots" }
         if (--snapshots == 0) {
             if (!applied) {
                 abandon()
@@ -936,13 +938,13 @@
     }
 
     private fun validateNotApplied() {
-        check(!applied) {
+        checkPrecondition(!applied) {
             "Unsupported operation on a snapshot that has been applied"
         }
     }
 
     private fun validateNotAppliedOrPinned() {
-        check(!applied || isPinned) {
+        checkPrecondition(!applied || isPinned) {
             "Unsupported operation on a disposed or applied snapshot"
         }
     }
diff --git a/compose/runtime/runtime/src/commonMain/kotlin/androidx/compose/runtime/snapshots/SnapshotStateList.kt b/compose/runtime/runtime/src/commonMain/kotlin/androidx/compose/runtime/snapshots/SnapshotStateList.kt
index a932d6b..22fda4b 100644
--- a/compose/runtime/runtime/src/commonMain/kotlin/androidx/compose/runtime/snapshots/SnapshotStateList.kt
+++ b/compose/runtime/runtime/src/commonMain/kotlin/androidx/compose/runtime/snapshots/SnapshotStateList.kt
@@ -19,8 +19,8 @@
 import androidx.compose.runtime.Stable
 import androidx.compose.runtime.external.kotlinx.collections.immutable.PersistentList
 import androidx.compose.runtime.external.kotlinx.collections.immutable.persistentListOf
+import androidx.compose.runtime.requirePrecondition
 import androidx.compose.runtime.synchronized
-import kotlin.jvm.JvmName
 
 /**
  * An implementation of [MutableList] that can be observed and snapshot. This is the result type
@@ -96,7 +96,7 @@
     override fun listIterator(): MutableListIterator<T> = StateListIterator(this, 0)
     override fun listIterator(index: Int): MutableListIterator<T> = StateListIterator(this, index)
     override fun subList(fromIndex: Int, toIndex: Int): MutableList<T> {
-        require(fromIndex in 0..toIndex && toIndex <= size) {
+        requirePrecondition(fromIndex in 0..toIndex && toIndex <= size) {
             "fromIndex or toIndex are out of bounds"
         }
         return SubList(this, fromIndex, toIndex)
@@ -472,7 +472,7 @@
     }
 
     override fun subList(fromIndex: Int, toIndex: Int): MutableList<T> {
-        require(fromIndex in 0..toIndex && toIndex <= size) {
+        requirePrecondition(fromIndex in 0..toIndex && toIndex <= size) {
             "fromIndex or toIndex are out of bounds"
         }
         validateModification()
diff --git a/compose/runtime/runtime/src/commonMain/kotlin/androidx/compose/runtime/snapshots/SnapshotStateObserver.kt b/compose/runtime/runtime/src/commonMain/kotlin/androidx/compose/runtime/snapshots/SnapshotStateObserver.kt
index c806f66..47c549d 100644
--- a/compose/runtime/runtime/src/commonMain/kotlin/androidx/compose/runtime/snapshots/SnapshotStateObserver.kt
+++ b/compose/runtime/runtime/src/commonMain/kotlin/androidx/compose/runtime/snapshots/SnapshotStateObserver.kt
@@ -30,6 +30,7 @@
 import androidx.compose.runtime.currentThreadId
 import androidx.compose.runtime.currentThreadName
 import androidx.compose.runtime.observeDerivedStateRecalculations
+import androidx.compose.runtime.requirePrecondition
 import androidx.compose.runtime.structuralEqualityPolicy
 
 /**
@@ -238,7 +239,7 @@
         val oldThreadId = currentMapThreadId
 
         if (oldThreadId != -1L) {
-            require(oldThreadId == currentThreadId()) {
+            requirePrecondition(oldThreadId == currentThreadId()) {
                 "Detected multithreaded access to SnapshotStateObserver: " +
                     "previousThreadId=$oldThreadId), " +
                     "currentThread={id=${currentThreadId()}, name=${currentThreadName()}}. " +
diff --git a/compose/ui/ui-graphics-lint/src/main/java/androidx/compose/ui/graphics/lint/ColorDetector.kt b/compose/ui/ui-graphics-lint/src/main/java/androidx/compose/ui/graphics/lint/ColorDetector.kt
index 8e5e6e3..f4fb0e4 100644
--- a/compose/ui/ui-graphics-lint/src/main/java/androidx/compose/ui/graphics/lint/ColorDetector.kt
+++ b/compose/ui/ui-graphics-lint/src/main/java/androidx/compose/ui/graphics/lint/ColorDetector.kt
@@ -31,8 +31,9 @@
 import com.android.tools.lint.detector.api.SourceCodeScanner
 import com.intellij.psi.PsiMethod
 import java.util.EnumSet
+import org.jetbrains.kotlin.psi.KtConstantExpression
 import org.jetbrains.uast.UCallExpression
-import org.jetbrains.uast.kotlin.KotlinULiteralExpression
+import org.jetbrains.uast.ULiteralExpression
 
 /**
  * [Detector] that checks hex Color definitions to ensure that they provide values for all four
@@ -49,8 +50,8 @@
             if (node.valueArgumentCount == 1) {
                 val argument = node.valueArguments.first()
                 // Ignore non-literal expressions
-                if (argument !is KotlinULiteralExpression) return
-                val argumentText = argument.sourcePsi.text ?: return
+                if (argument !is ULiteralExpression) return
+                val argumentText = (argument.sourcePsi as? KtConstantExpression)?.text ?: return
                 val hexPrefix = "0x"
                 val hexIndex = argumentText.indexOf(hexPrefix, ignoreCase = true)
                 // Ignore if this isn't a hex value
diff --git a/compose/ui/ui-inspection/src/main/cpp/CMakeLists.txt b/compose/ui/ui-inspection/src/main/cpp/CMakeLists.txt
index 2e027c8..417fea7 100644
--- a/compose/ui/ui-inspection/src/main/cpp/CMakeLists.txt
+++ b/compose/ui/ui-inspection/src/main/cpp/CMakeLists.txt
@@ -36,3 +36,4 @@
 target_include_directories(compose_inspection_jni
         PRIVATE ${CMAKE_CURRENT_BINARY_DIR}/external_jvmti)
 target_link_libraries(compose_inspection_jni ${android-lib} ${log-lib})
+target_link_options(compose_inspection_jni PRIVATE "-Wl,-z,max-page-size=16384")
diff --git a/compose/ui/ui-lint/build.gradle b/compose/ui/ui-lint/build.gradle
index debfd8e..acb5164 100644
--- a/compose/ui/ui-lint/build.gradle
+++ b/compose/ui/ui-lint/build.gradle
@@ -32,7 +32,7 @@
 BundleInsideHelper.forInsideLintJar(project)
 
 dependencies {
-    compileOnly(libs.androidLintMinComposeApi)
+    compileOnly(libs.androidLintApi)
     compileOnly(libs.kotlinStdlib)
 
     bundleInside(project(":compose:lint:common"))
diff --git a/compose/ui/ui-lint/src/main/java/androidx/compose/ui/lint/SuspiciousCompositionLocalModifierReadDetector.kt b/compose/ui/ui-lint/src/main/java/androidx/compose/ui/lint/SuspiciousCompositionLocalModifierReadDetector.kt
index 6911407..4e4b0cd 100644
--- a/compose/ui/ui-lint/src/main/java/androidx/compose/ui/lint/SuspiciousCompositionLocalModifierReadDetector.kt
+++ b/compose/ui/ui-lint/src/main/java/androidx/compose/ui/lint/SuspiciousCompositionLocalModifierReadDetector.kt
@@ -30,11 +30,11 @@
 import com.intellij.psi.PsiClass
 import com.intellij.psi.PsiMethod
 import java.util.EnumSet
+import org.jetbrains.uast.UBlockExpression
 import org.jetbrains.uast.UCallExpression
 import org.jetbrains.uast.UElement
+import org.jetbrains.uast.ULambdaExpression
 import org.jetbrains.uast.UMethod
-import org.jetbrains.uast.kotlin.KotlinUFunctionCallExpression
-import org.jetbrains.uast.kotlin.KotlinULambdaExpression
 
 @Suppress("UnstableApiUsage")
 class SuspiciousCompositionLocalModifierReadDetector : Detector(), SourceCodeScanner {
@@ -76,7 +76,7 @@
                 }
             }
             return
-        } else if (node is KotlinULambdaExpression.Body) {
+        } else if (node is UBlockExpression && node.uastParent is ULambdaExpression) {
             return
         }
 
@@ -90,7 +90,7 @@
     ) {
         if (node == null) {
             return
-        } else if (node is KotlinUFunctionCallExpression && node.isLazyDelegate()) {
+        } else if (node is UCallExpression && node.isLazyDelegate()) {
             report(context, usage) { localBeingRead ->
                 "Reading $localBeingRead lazily will only access the CompositionLocal's value " +
                     "once. To be notified of the latest value of the CompositionLocal, read " +
@@ -121,7 +121,7 @@
         this?.implementsListTypes
             ?.any { it.canonicalText == ClConsumerModifierNode } == true
 
-    private fun KotlinUFunctionCallExpression.isLazyDelegate(): Boolean =
+    private fun UCallExpression.isLazyDelegate(): Boolean =
         resolve()?.run { isInPackageName(Package("kotlin")) && name == "lazy" } == true
 
     companion object {
diff --git a/compose/ui/ui-lint/src/main/java/androidx/compose/ui/lint/SuspiciousModifierThenDetector.kt b/compose/ui/ui-lint/src/main/java/androidx/compose/ui/lint/SuspiciousModifierThenDetector.kt
new file mode 100644
index 0000000..c2d960b
--- /dev/null
+++ b/compose/ui/ui-lint/src/main/java/androidx/compose/ui/lint/SuspiciousModifierThenDetector.kt
@@ -0,0 +1,164 @@
+/*
+ * Copyright 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+@file:Suppress("UnstableApiUsage")
+
+package androidx.compose.ui.lint
+
+import androidx.compose.lint.Names
+import androidx.compose.lint.inheritsFrom
+import androidx.compose.lint.isInPackageName
+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.android.tools.lint.detector.api.isBelow
+import com.intellij.psi.PsiElement
+import com.intellij.psi.PsiMethod
+import java.util.EnumSet
+import org.jetbrains.kotlin.analysis.api.KtAnalysisSession
+import org.jetbrains.kotlin.analysis.api.analyze
+import org.jetbrains.kotlin.analysis.api.calls.KtCall
+import org.jetbrains.kotlin.analysis.api.calls.KtCallableMemberCall
+import org.jetbrains.kotlin.analysis.api.calls.KtCompoundAccessCall
+import org.jetbrains.kotlin.analysis.api.calls.KtImplicitReceiverValue
+import org.jetbrains.kotlin.analysis.api.calls.singleCallOrNull
+import org.jetbrains.kotlin.analysis.api.symbols.KtClassOrObjectSymbol
+import org.jetbrains.kotlin.analysis.api.symbols.KtReceiverParameterSymbol
+import org.jetbrains.kotlin.psi.KtCallExpression
+import org.jetbrains.kotlin.psi.KtExpression
+import org.jetbrains.uast.UCallExpression
+import org.jetbrains.uast.visitor.AbstractUastVisitor
+
+/**
+ * [Detector] that checks calls to Modifier.then to make sure the parameter does not contain a
+ * Modifier factory function called with an receiver, as this will cause duplicate modifiers in the
+ * chain. E.g. this.then(foo()), will result in this.then(this.then(foo)), as foo() internally will
+ * call this.then(FooModifier).
+ */
+class SuspiciousModifierThenDetector : Detector(), SourceCodeScanner {
+    override fun getApplicableMethodNames(): List<String> = listOf(ThenName)
+
+    override fun visitMethodCall(context: JavaContext, node: UCallExpression, method: PsiMethod) {
+        if (!method.isInPackageName(Names.Ui.PackageName)) return
+
+        val otherModifierArgument = node.valueArguments.firstOrNull() ?: return
+        val otherModifierArgumentSource = otherModifierArgument.sourcePsi ?: return
+
+        otherModifierArgument.accept(object : AbstractUastVisitor() {
+            /**
+             * Visit all calls to look for calls to a Modifier factory with implicit receiver
+             */
+            override fun visitCallExpression(node: UCallExpression): Boolean {
+                val hasModifierReceiverType =
+                    node.receiverType?.inheritsFrom(Names.Ui.Modifier) == true
+                val usesImplicitThis = node.receiver == null
+
+                if (!hasModifierReceiverType || !usesImplicitThis) {
+                    return false
+                }
+
+                val ktCallExpression = node.sourcePsi as? KtCallExpression ?: return false
+                // Resolve the implicit `this` to its source, if possible.
+                val implicitReceiver = analyze(ktCallExpression) {
+                    getImplicitReceiverValue(ktCallExpression)?.getImplicitReceiverPsi()
+                }
+
+                // The receiver used by the modifier function is defined within the then() call,
+                // such as then(Modifier.composed { otherModifierFactory() }). We don't know what
+                // the value of this receiver will be, so we ignore this case.
+                if (implicitReceiver.isBelow(otherModifierArgumentSource)) {
+                    return false
+                }
+
+                context.report(
+                    SuspiciousModifierThen,
+                    node,
+                    context.getNameLocation(node),
+                    "Using Modifier.then with a Modifier factory function with an implicit receiver"
+                )
+
+                // Keep on searching for more errors
+                return false
+            }
+        })
+    }
+
+    companion object {
+        val SuspiciousModifierThen = Issue.create(
+            "SuspiciousModifierThen",
+            "Using Modifier.then with a Modifier factory function with an implicit receiver",
+            "Calling a Modifier factory function with an implicit receiver inside " +
+                "Modifier.then will result in the receiver (`this`) being added twice to the " +
+                "chain. For example, fun Modifier.myModifier() = this.then(otherModifier()) - " +
+                "the implementation of factory functions such as Modifier.otherModifier() will " +
+                "internally call this.then(...) to chain the provided modifier with their " +
+                "implementation. When you expand this.then(otherModifier()), it becomes: " +
+                "this.then(this.then(OtherModifierImplementation)) - so you can see that `this` " +
+                "is included twice in the chain, which results in modifiers such as padding " +
+                "being applied twice, for example. Instead, you should either remove the then() " +
+                "and directly chain the factory function on the receiver, this.otherModifier(), " +
+                "or add the empty Modifier as the receiver for the factory, such as " +
+                "this.then(Modifier.otherModifier())",
+            Category.CORRECTNESS,
+            3,
+            Severity.ERROR,
+            Implementation(
+                SuspiciousModifierThenDetector::class.java,
+                EnumSet.of(Scope.JAVA_FILE, Scope.TEST_SOURCES)
+            )
+        )
+    }
+}
+
+private const val ThenName = "then"
+
+// Below functions taken from AnalysisApiLintUtils.kt
+
+/**
+ * Returns the PSI for [this], which will be the owning lambda expression or the surrounding class.
+ */
+private fun KtImplicitReceiverValue.getImplicitReceiverPsi(): PsiElement? {
+    return when (val receiverParameterSymbol = this.symbol) {
+        // the owning lambda expression
+        is KtReceiverParameterSymbol -> receiverParameterSymbol.owningCallableSymbol.psi
+        // the class that we are in, calling a method
+        is KtClassOrObjectSymbol -> receiverParameterSymbol.psi
+        else -> null
+    }
+}
+
+/**
+ * Returns the implicit receiver value of the call-like expression [ktExpression] (can include
+ * property accesses, for example).
+ */
+private fun KtAnalysisSession.getImplicitReceiverValue(
+    ktExpression: KtExpression
+): KtImplicitReceiverValue? {
+    val partiallyAppliedSymbol =
+        when (val call = ktExpression.resolveCall()?.singleCallOrNull<KtCall>()) {
+            is KtCompoundAccessCall -> call.compoundAccess.operationPartiallyAppliedSymbol
+            is KtCallableMemberCall<*, *> -> call.partiallyAppliedSymbol
+            else -> null
+        } ?: return null
+
+    return partiallyAppliedSymbol.extensionReceiver as? KtImplicitReceiverValue
+        ?: partiallyAppliedSymbol.dispatchReceiver as? KtImplicitReceiverValue
+}
diff --git a/compose/ui/ui-lint/src/main/java/androidx/compose/ui/lint/UiIssueRegistry.kt b/compose/ui/ui-lint/src/main/java/androidx/compose/ui/lint/UiIssueRegistry.kt
index 4332db6..78e535b9 100644
--- a/compose/ui/ui-lint/src/main/java/androidx/compose/ui/lint/UiIssueRegistry.kt
+++ b/compose/ui/ui-lint/src/main/java/androidx/compose/ui/lint/UiIssueRegistry.kt
@@ -39,6 +39,7 @@
         MultipleAwaitPointerEventScopesDetector.MultipleAwaitPointerEventScopes,
         ReturnFromAwaitPointerEventScopeDetector.ExitAwaitPointerEventScope,
         SuspiciousCompositionLocalModifierReadDetector.SuspiciousCompositionLocalModifierRead,
+        SuspiciousModifierThenDetector.SuspiciousModifierThen
     )
     override val vendor = Vendor(
         vendorName = "Jetpack Compose",
diff --git a/compose/ui/ui-lint/src/test/java/androidx/compose/ui/lint/ApiLintVersionsTest.kt b/compose/ui/ui-lint/src/test/java/androidx/compose/ui/lint/ApiLintVersionsTest.kt
index 1c0b6e9..567f6d43 100644
--- a/compose/ui/ui-lint/src/test/java/androidx/compose/ui/lint/ApiLintVersionsTest.kt
+++ b/compose/ui/ui-lint/src/test/java/androidx/compose/ui/lint/ApiLintVersionsTest.kt
@@ -34,6 +34,6 @@
 
         val registry = UiIssueRegistry()
         assertThat(registry.api).isEqualTo(CURRENT_API)
-        assertThat(registry.minApi).isEqualTo(10)
+        assertThat(registry.minApi).isEqualTo(14)
     }
 }
diff --git a/compose/ui/ui-lint/src/test/java/androidx/compose/ui/lint/ComposedModifierDetectorTest.kt b/compose/ui/ui-lint/src/test/java/androidx/compose/ui/lint/ComposedModifierDetectorTest.kt
index 3d33d33..3c53513 100644
--- a/compose/ui/ui-lint/src/test/java/androidx/compose/ui/lint/ComposedModifierDetectorTest.kt
+++ b/compose/ui/ui-lint/src/test/java/androidx/compose/ui/lint/ComposedModifierDetectorTest.kt
@@ -122,7 +122,7 @@
                     this@test
                 }
 
-                fun Modifier.test(): Modifier {
+                fun Modifier.test2(): Modifier {
                     return composed {
                         this@test
                     }
diff --git a/compose/ui/ui-lint/src/test/java/androidx/compose/ui/lint/SuspiciousModifierThenDetectorTest.kt b/compose/ui/ui-lint/src/test/java/androidx/compose/ui/lint/SuspiciousModifierThenDetectorTest.kt
new file mode 100644
index 0000000..cd8c963
--- /dev/null
+++ b/compose/ui/ui-lint/src/test/java/androidx/compose/ui/lint/SuspiciousModifierThenDetectorTest.kt
@@ -0,0 +1,123 @@
+/*
+ * Copyright 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+@file:Suppress("UnstableApiUsage")
+
+package androidx.compose.ui.lint
+
+import androidx.compose.lint.test.Stubs
+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.JUnit4
+
+/* ktlint-disable max-line-length */
+
+/**
+ * Test for [SuspiciousModifierThenDetector].
+ */
+@RunWith(JUnit4::class)
+class SuspiciousModifierThenDetectorTest : LintDetectorTest() {
+    override fun getDetector(): Detector = SuspiciousModifierThenDetector()
+
+    override fun getIssues(): MutableList<Issue> =
+        mutableListOf(SuspiciousModifierThenDetector.SuspiciousModifierThen)
+
+    @Test
+    fun clean() {
+        lint().files(
+            kotlin(
+                """
+                package test
+
+                import androidx.compose.ui.Modifier
+
+                object TestModifier : Modifier.Element
+
+                fun Modifier.test(): Modifier = this.then(TestModifier)
+
+                fun Modifier.test2() = this.then(Modifier.test())
+
+                fun Modifier.test3() = this.then(with(Modifier) { test() })
+
+                fun Modifier.test4() = this.then(if (true) TestModifier else TestModifier)
+                fun Modifier.test5() = this.then(if (true) Modifier.test() else Modifier.test())
+
+                // We don't know what the receiver will be inside an arbitrary lambda like here, so
+                // we shouldn't warn for any calls inside a lambda
+                fun Modifier.composed(
+                    factory: Modifier.() -> Modifier
+                ): Modifier = then(with(Modifier) { factory() })
+
+                fun Modifier.test6() = this.then(Modifier.composed { TestModifier })
+                fun Modifier.test7() = this.then(Modifier.composed { test() })
+
+                fun Modifier.test8(): Modifier {
+                    val lambda: Modifier.() -> Modifier = {
+                        Modifier.test()
+                    }
+                    return this.then(lambda())
+                }
+"""
+            ),
+            Stubs.Modifier
+        )
+            .run()
+            .expectClean()
+    }
+
+    @Test
+    fun errors() {
+        lint().files(
+            kotlin(
+                """
+                package test
+
+                import androidx.compose.ui.Modifier
+
+                object TestModifier : Modifier.Element
+
+                fun Modifier.test(): Modifier = this.then(TestModifier)
+
+                fun Modifier.test2() = this.then(test())
+
+                fun Modifier.test3() = this.then(with(1) { test() })
+
+                fun Modifier.test4() = this.then(if (true) test() else TestModifier)
+"""
+            ),
+            Stubs.Modifier
+        )
+            .run()
+            .expect(
+                """
+src/test/TestModifier.kt:10: Error: Using Modifier.then with a Modifier factory function with an implicit receiver [SuspiciousModifierThen]
+                fun Modifier.test2() = this.then(test())
+                                                 ~~~~
+src/test/TestModifier.kt:12: Error: Using Modifier.then with a Modifier factory function with an implicit receiver [SuspiciousModifierThen]
+                fun Modifier.test3() = this.then(with(1) { test() })
+                                                           ~~~~
+src/test/TestModifier.kt:14: Error: Using Modifier.then with a Modifier factory function with an implicit receiver [SuspiciousModifierThen]
+                fun Modifier.test4() = this.then(if (true) test() else TestModifier)
+                                                           ~~~~
+3 errors, 0 warnings
+            """
+            )
+    }
+}
+/* ktlint-enable max-line-length */
diff --git a/compose/ui/ui-test/src/androidInstrumentedTest/kotlin/androidx/compose/ui/test/DeviceConfigurationOverrideTest.kt b/compose/ui/ui-test/src/androidInstrumentedTest/kotlin/androidx/compose/ui/test/DeviceConfigurationOverrideTest.kt
index f078a82..5210584 100644
--- a/compose/ui/ui-test/src/androidInstrumentedTest/kotlin/androidx/compose/ui/test/DeviceConfigurationOverrideTest.kt
+++ b/compose/ui/ui-test/src/androidInstrumentedTest/kotlin/androidx/compose/ui/test/DeviceConfigurationOverrideTest.kt
@@ -19,9 +19,11 @@
 import android.content.res.Configuration
 import android.util.DisplayMetrics
 import android.view.View
+import androidx.compose.foundation.layout.Box
 import androidx.compose.foundation.layout.Row
 import androidx.compose.foundation.layout.Spacer
 import androidx.compose.foundation.layout.requiredSize
+import androidx.compose.foundation.layout.size
 import androidx.compose.ui.Modifier
 import androidx.compose.ui.layout.layout
 import androidx.compose.ui.platform.LocalConfiguration
@@ -123,7 +125,85 @@
     }
 
     @Test
-    fun sizeOverride_allowsForCorrectSpace() {
+    fun sizeOverride_allowsForCorrectSpace_smallPortraitAspectRatio() {
+        lateinit var actualDensity: Density
+        var actualConstraints: Constraints? = null
+
+        rule.setContent {
+            DeviceConfigurationOverride(
+                DeviceConfigurationOverride.ForcedSize(DpSize(30.dp, 40.dp))
+            ) {
+                Spacer(
+                    modifier = Modifier.layout { measurable, constraints ->
+                        actualConstraints = constraints
+                        actualDensity = this
+
+                        val placeable = measurable.measure(constraints)
+
+                        layout(placeable.width, placeable.height) {
+                            placeable.placeRelative(0, 0)
+                        }
+                    }
+                )
+            }
+        }
+
+        // The constraint should be within 0.5 pixels of the specified size
+        // Due to rounding, we can't expect to have the Spacer take exactly the requested size which
+        // this is true in normal Compose code as well
+        assertEquals(
+            with(actualDensity) { 30.dp.toPx() },
+            actualConstraints!!.maxWidth.toFloat(),
+            0.5f
+        )
+        assertEquals(
+            with(actualDensity) { 40.dp.toPx() },
+            actualConstraints!!.maxHeight.toFloat(),
+            0.5f
+        )
+    }
+
+    @Test
+    fun sizeOverride_allowsForCorrectSpace_smallLandscapeAspectRatio() {
+        lateinit var actualDensity: Density
+        var actualConstraints: Constraints? = null
+
+        rule.setContent {
+            DeviceConfigurationOverride(
+                DeviceConfigurationOverride.ForcedSize(DpSize(40.dp, 30.dp))
+            ) {
+                Spacer(
+                    modifier = Modifier.layout { measurable, constraints ->
+                        actualConstraints = constraints
+                        actualDensity = this
+
+                        val placeable = measurable.measure(constraints)
+
+                        layout(placeable.width, placeable.height) {
+                            placeable.placeRelative(0, 0)
+                        }
+                    }
+                )
+            }
+        }
+
+        // The constraint should be within 0.5 pixels of the specified size
+        // Due to rounding, we can't expect to have the Spacer take exactly the requested size which
+        // this is true in normal Compose code as well
+        assertEquals(
+            with(actualDensity) { 40.dp.toPx() },
+            actualConstraints!!.maxWidth.toFloat(),
+            0.5f
+        )
+        assertEquals(
+            with(actualDensity) { 30.dp.toPx() },
+            actualConstraints!!.maxHeight.toFloat(),
+            0.5f
+        )
+    }
+
+    @Test
+    fun sizeOverride_allowsForCorrectSpace_largePortraitAspectRatio() {
         lateinit var actualDensity: Density
         var actualConstraints: Constraints? = null
 
@@ -146,29 +226,170 @@
             }
         }
 
-        assertTrue(with(actualDensity) { actualConstraints!!.maxWidth.toDp() >= 3000.dp })
-        assertTrue(with(actualDensity) { actualConstraints!!.maxHeight.toDp() >= 4000.dp })
+        // The constraint should be within 0.5 pixels of the specified size
+        // Due to rounding, we can't expect to have the Spacer take exactly the requested size which
+        // this is true in normal Compose code as well
+        assertEquals(
+            with(actualDensity) { 3000.dp.toPx() },
+            actualConstraints!!.maxWidth.toFloat(),
+            0.5f
+        )
+        assertEquals(
+            with(actualDensity) { 4000.dp.toPx() },
+            actualConstraints!!.maxHeight.toFloat(),
+            0.5f
+        )
     }
 
     @Test
-    fun sizeOverride_overridesConfigurationDensity() {
-        lateinit var density: Density
-        lateinit var configuration: Configuration
+    fun sizeOverride_allowsForCorrectSpace_largeLandscapeAspectRatio() {
+        lateinit var actualDensity: Density
+        var actualConstraints: Constraints? = null
 
         rule.setContent {
             DeviceConfigurationOverride(
+                DeviceConfigurationOverride.ForcedSize(DpSize(4000.dp, 3000.dp))
+            ) {
+                Spacer(
+                    modifier = Modifier.layout { measurable, constraints ->
+                        actualConstraints = constraints
+                        actualDensity = this
+
+                        val placeable = measurable.measure(constraints)
+
+                        layout(placeable.width, placeable.height) {
+                            placeable.placeRelative(0, 0)
+                        }
+                    }
+                )
+            }
+        }
+
+        // The constraint should be within 0.5 pixels of the specified size
+        // Due to rounding, we can't expect to have the Spacer take exactly the requested size which
+        // this is true in normal Compose code as well
+        assertEquals(
+            with(actualDensity) { 4000.dp.toPx() },
+            actualConstraints!!.maxWidth.toFloat(),
+            0.5f
+        )
+        assertEquals(
+            with(actualDensity) { 3000.dp.toPx() },
+            actualConstraints!!.maxHeight.toFloat(),
+            0.5f
+        )
+    }
+
+    @Test
+    fun sizeOverride_largeRequestedSize_overridesConfigurationDensity() {
+        lateinit var originalDensity: Density
+        lateinit var overriddenDensity: Density
+        lateinit var overriddenConfiguration: Configuration
+
+        rule.setContent {
+            originalDensity = LocalDensity.current
+            DeviceConfigurationOverride(
                 DeviceConfigurationOverride.ForcedSize(DpSize(3000.dp, 4000.dp))
             ) {
-                density = LocalDensity.current
-                configuration = LocalConfiguration.current
+                overriddenDensity = LocalDensity.current
+                overriddenConfiguration = LocalConfiguration.current
+            }
+        }
+
+        // A 3000dp by 4000dp device is so big, that we can assume that the density needs to be
+        // overridden.
+        // If this test runs on a device with that size screen, where overriding density is not
+        // necessary, this test might fail. If that is happening, hopefully the future is a nice
+        // place.
+        assertTrue(originalDensity.density > overriddenDensity.density)
+
+        // Convert the Configuration's density in DPI to the raw float multiplier
+        val overriddenConfigurationDensityMultiplier =
+            overriddenConfiguration.densityDpi.toFloat() / DisplayMetrics.DENSITY_DEFAULT
+
+        assertEquals(
+            overriddenDensity.density,
+            overriddenConfigurationDensityMultiplier,
+            // Compare within half a step of density DPI changes
+            1f / DisplayMetrics.DENSITY_DEFAULT / 2f
+        )
+    }
+
+    @Test
+    fun sizeOverride_notNeededForPortrait_doesNotOverrideConfigurationDensity() {
+        lateinit var originalDensity: Density
+        lateinit var overriddenDensity: Density
+        lateinit var overriddenConfiguration: Configuration
+
+        rule.setContent {
+            originalDensity = LocalDensity.current
+            Box(Modifier.size(35.dp, 45.dp)) {
+                DeviceConfigurationOverride(
+                    DeviceConfigurationOverride.ForcedSize(DpSize(30.dp, 40.dp))
+                ) {
+                    overriddenDensity = LocalDensity.current
+                    overriddenConfiguration = LocalConfiguration.current
+                }
             }
         }
 
         // This is a strict equality for floating point values which is normally problematic, but
-        // this should be precisely equal
+        // these should be precisely equal
         assertEquals(
-            density.density,
-            configuration.densityDpi.toFloat() / DisplayMetrics.DENSITY_DEFAULT
+            originalDensity.density,
+            overriddenDensity.density
+        )
+
+        // Convert the Configuration's density in DPI to the raw float multiplier
+        val overriddenConfigurationDensityMultiplier =
+            overriddenConfiguration.densityDpi.toFloat() / DisplayMetrics.DENSITY_DEFAULT
+
+        // This is a strict equality for floating point values which is normally problematic, but
+        // these should be precisely equal
+        assertEquals(
+            overriddenDensity.density,
+            overriddenConfigurationDensityMultiplier,
+            // Compare within half a step of density DPI changes
+            1f / DisplayMetrics.DENSITY_DEFAULT / 2f
+        )
+    }
+
+    @Test
+    fun sizeOverride_notNeededForLandscape_doesNotOverrideConfigurationDensity() {
+        lateinit var originalDensity: Density
+        lateinit var overriddenDensity: Density
+        lateinit var overriddenConfiguration: Configuration
+
+        rule.setContent {
+            originalDensity = LocalDensity.current
+            Box(Modifier.size(45.dp, 35.dp)) {
+                DeviceConfigurationOverride(
+                    DeviceConfigurationOverride.ForcedSize(DpSize(40.dp, 30.dp))
+                ) {
+                    overriddenDensity = LocalDensity.current
+                    overriddenConfiguration = LocalConfiguration.current
+                }
+            }
+        }
+
+        // This is a strict equality for floating point values which is normally problematic, but
+        // these should be precisely equal
+        assertEquals(
+            originalDensity.density,
+            overriddenDensity.density
+        )
+
+        // Convert the Configuration's density in DPI to the raw float multiplier
+        val overriddenConfigurationDensityMultiplier =
+            overriddenConfiguration.densityDpi.toFloat() / DisplayMetrics.DENSITY_DEFAULT
+
+        // This is a strict equality for floating point values which is normally problematic, but
+        // these should be precisely equal
+        assertEquals(
+            overriddenDensity.density,
+            overriddenConfigurationDensityMultiplier,
+            // Compare within half a step of density DPI changes
+            1f / DisplayMetrics.DENSITY_DEFAULT / 2f
         )
     }
 
diff --git a/compose/ui/ui-test/src/commonMain/kotlin/androidx/compose/ui/test/DensityForcedSize.kt b/compose/ui/ui-test/src/commonMain/kotlin/androidx/compose/ui/test/DensityForcedSize.kt
index c393dcb..b735798 100644
--- a/compose/ui/ui-test/src/commonMain/kotlin/androidx/compose/ui/test/DensityForcedSize.kt
+++ b/compose/ui/ui-test/src/commonMain/kotlin/androidx/compose/ui/test/DensityForcedSize.kt
@@ -46,13 +46,6 @@
 ) {
     SubcomposeLayout(
         modifier = modifier
-            .then(
-                if (size.isSpecified) {
-                    Modifier.size(size)
-                } else {
-                    Modifier
-                }
-            )
     ) { constraints ->
         val measurables = subcompose(Unit) {
             val maxWidth = constraints.maxWidth.toDp()
@@ -63,7 +56,7 @@
                 maxWidth
             }
             val requiredHeight = if (size.isSpecified) {
-                max(maxWidth, size.height)
+                max(maxHeight, size.height)
             } else {
                 maxHeight
             }
diff --git a/compose/ui/ui-text/api/current.ignore b/compose/ui/ui-text/api/current.ignore
index 6aede09..2900208 100644
--- a/compose/ui/ui-text/api/current.ignore
+++ b/compose/ui/ui-text/api/current.ignore
@@ -25,3 +25,7 @@
 
 RemovedClass: androidx.compose.ui.text.input.AndroidImeOptions:
     Removed class androidx.compose.ui.text.input.AndroidImeOptions
+
+
+RemovedPackage: androidx.compose.ui.text.platform:
+    Removed package androidx.compose.ui.text.platform
diff --git a/compose/ui/ui-text/api/current.txt b/compose/ui/ui-text/api/current.txt
index 7196bff..f3272ff 100644
--- a/compose/ui/ui-text/api/current.txt
+++ b/compose/ui/ui-text/api/current.txt
@@ -1320,15 +1320,6 @@
 
 }
 
-package androidx.compose.ui.text.platform {
-
-  @SuppressCompatibility @androidx.compose.ui.text.InternalTextApi public final class URLSpanCache {
-    ctor public URLSpanCache();
-    method public android.text.style.URLSpan toURLSpan(androidx.compose.ui.text.UrlAnnotation urlAnnotation);
-  }
-
-}
-
 package androidx.compose.ui.text.platform.extensions {
 
   public final class TtsAnnotationExtensions_androidKt {
diff --git a/compose/ui/ui-text/api/restricted_current.txt b/compose/ui/ui-text/api/restricted_current.txt
index 9e9bcf6..f6a490e 100644
--- a/compose/ui/ui-text/api/restricted_current.txt
+++ b/compose/ui/ui-text/api/restricted_current.txt
@@ -1329,11 +1329,6 @@
   @kotlin.PublishedApi internal final class SynchronizedObject {
   }
 
-  @SuppressCompatibility @androidx.compose.ui.text.InternalTextApi public final class URLSpanCache {
-    ctor public URLSpanCache();
-    method public android.text.style.URLSpan toURLSpan(androidx.compose.ui.text.UrlAnnotation urlAnnotation);
-  }
-
 }
 
 package androidx.compose.ui.text.platform.extensions {
diff --git a/compose/ui/ui-text/build.gradle b/compose/ui/ui-text/build.gradle
index dc89b2a..0eeb331 100644
--- a/compose/ui/ui-text/build.gradle
+++ b/compose/ui/ui-text/build.gradle
@@ -84,7 +84,7 @@
                 api("androidx.annotation:annotation-experimental:1.4.0")
                 implementation("androidx.core:core:1.7.0")
                 implementation("androidx.emoji2:emoji2:1.2.0")
-                implementation('androidx.collection:collection:1.0.0')
+                implementation("androidx.collection:collection:1.4.0")
             }
         }
 
diff --git a/compose/ui/ui-text/src/androidInstrumentedTest/kotlin/androidx/compose/ui/text/platform/AndroidAccessibilitySpannableStringTest.kt b/compose/ui/ui-text/src/androidInstrumentedTest/kotlin/androidx/compose/ui/text/platform/AndroidAccessibilitySpannableStringTest.kt
index 8976a8b..e8025f9 100644
--- a/compose/ui/ui-text/src/androidInstrumentedTest/kotlin/androidx/compose/ui/text/platform/AndroidAccessibilitySpannableStringTest.kt
+++ b/compose/ui/ui-text/src/androidInstrumentedTest/kotlin/androidx/compose/ui/text/platform/AndroidAccessibilitySpannableStringTest.kt
@@ -20,6 +20,7 @@
 import android.text.SpannableString
 import android.text.style.AbsoluteSizeSpan
 import android.text.style.BackgroundColorSpan
+import android.text.style.ClickableSpan
 import android.text.style.ForegroundColorSpan
 import android.text.style.LocaleSpan
 import android.text.style.RelativeSizeSpan
@@ -30,10 +31,12 @@
 import android.text.style.TypefaceSpan
 import android.text.style.URLSpan
 import android.text.style.UnderlineSpan
+import androidx.collection.longObjectMapOf
 import androidx.compose.ui.graphics.Color
 import androidx.compose.ui.graphics.toArgb
 import androidx.compose.ui.text.ExperimentalTextApi
 import androidx.compose.ui.text.InternalTextApi
+import androidx.compose.ui.text.LinkAnnotation
 import androidx.compose.ui.text.SpanStyle
 import androidx.compose.ui.text.UncachedFontFamilyResolver
 import androidx.compose.ui.text.UrlAnnotation
@@ -55,6 +58,7 @@
 import androidx.compose.ui.unit.Density
 import androidx.compose.ui.unit.em
 import androidx.compose.ui.unit.sp
+import androidx.compose.ui.util.packInts
 import androidx.test.ext.junit.runners.AndroidJUnit4
 import androidx.test.filters.SmallTest
 import androidx.test.platform.app.InstrumentationRegistry
@@ -84,7 +88,9 @@
         val spannableString = annotatedString.toAccessibilitySpannableString(
             density,
             fontFamilyResolver,
-            urlSpanCache
+            urlSpanCache,
+            null,
+            0
         )
 
         assertThat(spannableString).isInstanceOf(SpannableString::class.java)
@@ -109,7 +115,9 @@
         val spannableString = annotatedString.toAccessibilitySpannableString(
             density,
             fontFamilyResolver,
-            urlSpanCache
+            urlSpanCache,
+            null,
+            0
         )
 
         assertThat(spannableString).isInstanceOf(SpannableString::class.java)
@@ -133,7 +141,9 @@
         val spannableString = annotatedString.toAccessibilitySpannableString(
             density,
             fontFamilyResolver,
-            urlSpanCache
+            urlSpanCache,
+            null,
+            0
         )
 
         assertThat(spannableString).isInstanceOf(SpannableString::class.java)
@@ -157,7 +167,9 @@
         val spannableString = annotatedString.toAccessibilitySpannableString(
             density,
             fontFamilyResolver,
-            urlSpanCache
+            urlSpanCache,
+            null,
+            0
         )
 
         assertThat(spannableString).isInstanceOf(SpannableString::class.java)
@@ -180,7 +192,9 @@
         val spannableString = annotatedString.toAccessibilitySpannableString(
             density,
             fontFamilyResolver,
-            urlSpanCache
+            urlSpanCache,
+            null,
+            0
         )
 
         assertThat(spannableString).isInstanceOf(SpannableString::class.java)
@@ -203,7 +217,9 @@
         val spannableString = annotatedString.toAccessibilitySpannableString(
             density,
             fontFamilyResolver,
-            urlSpanCache
+            urlSpanCache,
+            null,
+            0
         )
 
         assertThat(spannableString).isInstanceOf(SpannableString::class.java)
@@ -227,7 +243,9 @@
         val spannableString = annotatedString.toAccessibilitySpannableString(
             density,
             fontFamilyResolver,
-            urlSpanCache
+            urlSpanCache,
+            null,
+            0
         )
 
         assertThat(spannableString).isInstanceOf(SpannableString::class.java)
@@ -252,7 +270,9 @@
         val spannableString = annotatedString.toAccessibilitySpannableString(
             density,
             fontFamilyResolver,
-            urlSpanCache
+            urlSpanCache,
+            null,
+            0
         )
 
         assertThat(spannableString).isInstanceOf(SpannableString::class.java)
@@ -273,7 +293,9 @@
         val spannableString = annotatedString.toAccessibilitySpannableString(
             density,
             fontFamilyResolver,
-            urlSpanCache
+            urlSpanCache,
+            null,
+            0
         )
 
         assertThat(spannableString).isInstanceOf(SpannableString::class.java)
@@ -297,7 +319,9 @@
         val spannableString = annotatedString.toAccessibilitySpannableString(
             density,
             fontFamilyResolver,
-            urlSpanCache
+            urlSpanCache,
+            null,
+            0
         )
 
         assertThat(spannableString).isInstanceOf(SpannableString::class.java)
@@ -321,7 +345,9 @@
         val spannableString = annotatedString.toAccessibilitySpannableString(
             density,
             fontFamilyResolver,
-            urlSpanCache
+            urlSpanCache,
+            null,
+            0
         )
 
         assertThat(spannableString).isInstanceOf(SpannableString::class.java)
@@ -344,7 +370,9 @@
         val spannableString = annotatedString.toAccessibilitySpannableString(
             density,
             fontFamilyResolver,
-            urlSpanCache
+            urlSpanCache,
+            null,
+            0
         )
 
         assertThat(spannableString).isInstanceOf(SpannableString::class.java)
@@ -368,7 +396,9 @@
         val spannableString = annotatedString.toAccessibilitySpannableString(
             density,
             fontFamilyResolver,
-            urlSpanCache
+            urlSpanCache,
+            null,
+            0
         )
 
         assertThat(spannableString).isInstanceOf(SpannableString::class.java)
@@ -393,7 +423,7 @@
         val fontFamilyResolver = createFontFamilyResolver(context)
 
         // see if font span is added
-        string.toAccessibilitySpannableString(density, fontFamilyResolver, URLSpanCache())
+        string.toAccessibilitySpannableString(density, fontFamilyResolver, URLSpanCache(), null, 0)
 
         // toAccessibilitySpannableString should _not_ make any font requests
         assertThat(loader.blockingRequests).isEmpty()
@@ -411,14 +441,174 @@
             append("link")
         }
 
-        val spannable1 =
-            string.toAccessibilitySpannableString(density, fontFamilyResolver, urlSpanCache)
-        val spannable2 =
-            string.toAccessibilitySpannableString(density, fontFamilyResolver, urlSpanCache)
+        val spannable1 = string.toAccessibilitySpannableString(
+            density, fontFamilyResolver, urlSpanCache, null, 0
+        )
+        val spannable2 = string.toAccessibilitySpannableString(
+            density, fontFamilyResolver, urlSpanCache, null, 0
+        )
         val urlSpan1 = spannable1.getSpans(0, string.length, URLSpan::class.java).single()
         val urlSpan2 = spannable2.getSpans(0, string.length, URLSpan::class.java).single()
 
         assertThat(spannable1).isNotSameInstanceAs(spannable2)
         assertThat(urlSpan1).isSameInstanceAs(urlSpan2)
     }
+
+    @Test
+    fun urlSpansSame_forSameAnnotationAndRange() {
+        val link = LinkAnnotation.Url("url")
+
+        val string = buildAnnotatedString {
+            pushLink(link)
+            append("link")
+        }
+
+        val spannable1 = string.toAccessibilitySpannableString(
+            density, fontFamilyResolver, urlSpanCache, null, 0
+        )
+        val spannable2 = string.toAccessibilitySpannableString(
+            density, fontFamilyResolver, urlSpanCache, null, 0
+        )
+        val urlSpan1 = spannable1.getSpans(0, string.length, URLSpan::class.java).single()
+        val urlSpan2 = spannable2.getSpans(0, string.length, URLSpan::class.java).single()
+
+        assertThat(spannable1).isNotSameInstanceAs(spannable2)
+        assertThat(urlSpan1).isSameInstanceAs(urlSpan2)
+    }
+
+    @Test
+    fun clickableSpansSame_forSameAnnotationAndRange_forUrlsWithCallback() {
+        val link = LinkAnnotation.Url("url")
+        val linkActions = longObjectMapOf(packInts(0, 4), {})
+
+        val string = buildAnnotatedString {
+            pushLink(link)
+            append("link")
+        }
+
+        val spannable1 = string.toAccessibilitySpannableString(
+            density, fontFamilyResolver, urlSpanCache, linkActions, 0
+        )
+        val spannable2 = string.toAccessibilitySpannableString(
+            density, fontFamilyResolver, urlSpanCache, linkActions, 0
+        )
+        val span1 = spannable1.getSpans(0, string.length, ClickableSpan::class.java).single()
+        val span2 = spannable2.getSpans(0, string.length, ClickableSpan::class.java).single()
+
+        assertThat(spannable1).isNotSameInstanceAs(spannable2)
+        assertThat(span1).isSameInstanceAs(span2)
+    }
+
+    @Test
+    fun clickableSpansSame_forSameAnnotationAndRange_forClickables() {
+        val link = LinkAnnotation.Clickable("Tag")
+        val linkActions = longObjectMapOf(packInts(0, 9), {})
+
+        val string = buildAnnotatedString {
+            pushLink(link)
+            append("clickable")
+        }
+
+        val spannable1 = string.toAccessibilitySpannableString(
+            density, fontFamilyResolver, urlSpanCache, linkActions, 0
+        )
+        val spannable2 = string.toAccessibilitySpannableString(
+            density, fontFamilyResolver, urlSpanCache, linkActions, 0
+        )
+        val span1 = spannable1.getSpans(0, string.length, ClickableSpan::class.java).single()
+        val span2 = spannable2.getSpans(0, string.length, ClickableSpan::class.java).single()
+
+        assertThat(spannable1).isNotSameInstanceAs(spannable2)
+        assertThat(span1).isSameInstanceAs(span2)
+    }
+
+    @Test
+    fun clickableSpansDifferent_forSameAnnotationButDifferentRange_forUrlsWithCallback() {
+        val link = LinkAnnotation.Url("url")
+        val linkActions = longObjectMapOf(
+            packInts(0, 4), {},
+            packInts(4, 8), {}
+        )
+
+        val string = buildAnnotatedString {
+            withAnnotation(link) { append("link") }
+            withAnnotation(link) { append("link") }
+        }
+
+        val spannable = string.toAccessibilitySpannableString(
+            density, fontFamilyResolver, urlSpanCache, linkActions, 0
+        )
+        val span1 = spannable.getSpans(0, string.length, ClickableSpan::class.java)[0]
+        val span2 = spannable.getSpans(0, string.length, ClickableSpan::class.java)[1]
+
+        assertThat(span1).isNotSameInstanceAs(span2)
+    }
+
+    @Test
+    fun clickableSpansDifferent_forSameAnnotatedStringWithDifferentRange_forClickables() {
+        val link = LinkAnnotation.Clickable("tag")
+        val linkActions = longObjectMapOf(
+            packInts(0, 4), {},
+            packInts(4, 8), {}
+        )
+
+        val string = buildAnnotatedString {
+            withAnnotation(link) { append("link") }
+            withAnnotation(link) { append("link") }
+        }
+
+        val spannable = string.toAccessibilitySpannableString(
+            density, fontFamilyResolver, urlSpanCache, linkActions, 0
+        )
+        val span1 = spannable.getSpans(0, string.length, ClickableSpan::class.java)[0]
+        val span2 = spannable.getSpans(0, string.length, ClickableSpan::class.java)[1]
+
+        assertThat(span1).isNotSameInstanceAs(span2)
+    }
+
+    @Test
+    fun clickableSpansDifferent_forSameAnnotationAndRange_differentNodeInfo_forUrlsWithCallback() {
+        val link = LinkAnnotation.Url("url")
+        val linkActions = longObjectMapOf(packInts(0, 4), {})
+
+        val string = buildAnnotatedString {
+            withAnnotation(link) { append("link") }
+            withAnnotation(link) { append("link") }
+        }
+
+        val spannable1 = string.toAccessibilitySpannableString(
+            density, fontFamilyResolver, urlSpanCache, linkActions, 0
+        )
+        val spannable2 = string.toAccessibilitySpannableString(
+            density, fontFamilyResolver, urlSpanCache, linkActions, 1
+        )
+        val span1 = spannable1.getSpans(0, string.length, ClickableSpan::class.java)[0]
+        val span2 = spannable2.getSpans(0, string.length, ClickableSpan::class.java)[0]
+
+        assertThat(spannable1).isNotSameInstanceAs(spannable2)
+        assertThat(span1).isNotSameInstanceAs(span2)
+    }
+
+    @Test
+    fun clickableSpansDifferent_forSameAnnotationAndRange_differentNodeInfo_forClickables() {
+        val link = LinkAnnotation.Clickable("tag")
+        val linkActions = longObjectMapOf(packInts(0, 4), {})
+
+        val string = buildAnnotatedString {
+            withAnnotation(link) { append("link") }
+            withAnnotation(link) { append("link") }
+        }
+
+        val spannable1 = string.toAccessibilitySpannableString(
+            density, fontFamilyResolver, urlSpanCache, linkActions, 0
+        )
+        val spannable2 = string.toAccessibilitySpannableString(
+            density, fontFamilyResolver, urlSpanCache, linkActions, 1
+        )
+        val span1 = spannable1.getSpans(0, string.length, ClickableSpan::class.java)[0]
+        val span2 = spannable2.getSpans(0, string.length, ClickableSpan::class.java)[0]
+
+        assertThat(spannable1).isNotSameInstanceAs(spannable2)
+        assertThat(span1).isNotSameInstanceAs(span2)
+    }
 }
diff --git a/compose/ui/ui-text/src/androidMain/kotlin/androidx/compose/ui/text/platform/AndroidAccessibilitySpannableString.android.kt b/compose/ui/ui-text/src/androidMain/kotlin/androidx/compose/ui/text/platform/AndroidAccessibilitySpannableString.android.kt
index 0e0bb15..9574329 100644
--- a/compose/ui/ui-text/src/androidMain/kotlin/androidx/compose/ui/text/platform/AndroidAccessibilitySpannableString.android.kt
+++ b/compose/ui/ui-text/src/androidMain/kotlin/androidx/compose/ui/text/platform/AndroidAccessibilitySpannableString.android.kt
@@ -28,9 +28,11 @@
 import androidx.annotation.DoNotInline
 import androidx.annotation.RequiresApi
 import androidx.annotation.RestrictTo
+import androidx.collection.LongObjectMap
 import androidx.compose.ui.text.AnnotatedString
 import androidx.compose.ui.text.ExperimentalTextApi
 import androidx.compose.ui.text.InternalTextApi
+import androidx.compose.ui.text.LinkAnnotation
 import androidx.compose.ui.text.SpanStyle
 import androidx.compose.ui.text.font.FontFamily
 import androidx.compose.ui.text.font.FontStyle
@@ -53,7 +55,9 @@
 fun AnnotatedString.toAccessibilitySpannableString(
     density: Density,
     fontFamilyResolver: FontFamily.Resolver,
-    urlSpanCache: URLSpanCache
+    urlSpanCache: URLSpanCache,
+    linkActions: LongObjectMap<() -> Unit>?,
+    accessibilityNodeId: Int
 ): SpannableString {
     val spannableString = SpannableString(text)
     spanStylesOrNull?.fastForEach { (style, start, end) ->
@@ -81,6 +85,27 @@
         )
     }
 
+    getLinkAnnotations(0, length).fastForEach { linkRange ->
+        when {
+            linkActions != null && linkActions.isNotEmpty() -> {
+                spannableString.setSpan(
+                    urlSpanCache.toClickableSpan(linkRange, linkActions, accessibilityNodeId),
+                    linkRange.start,
+                    linkRange.end,
+                    Spanned.SPAN_EXCLUSIVE_EXCLUSIVE
+                )
+            }
+            linkRange.item is LinkAnnotation.Url -> {
+                spannableString.setSpan(
+                    urlSpanCache.toURLSpan(linkRange.toUrl()),
+                    linkRange.start,
+                    linkRange.end,
+                    Spanned.SPAN_EXCLUSIVE_EXCLUSIVE
+                )
+            }
+        }
+    }
+
     return spannableString
 }
 
@@ -179,3 +204,6 @@
     @DoNotInline
     fun createTypefaceSpan(typeface: Typeface): TypefaceSpan = TypefaceSpan(typeface)
 }
+
+private fun AnnotatedString.Range<LinkAnnotation>.toUrl() =
+    AnnotatedString.Range(this.item as LinkAnnotation.Url, this.start, this.end)
diff --git a/compose/ui/ui-text/src/androidMain/kotlin/androidx/compose/ui/text/platform/URLSpanCache.android.kt b/compose/ui/ui-text/src/androidMain/kotlin/androidx/compose/ui/text/platform/URLSpanCache.android.kt
index f50503b..522c6a6 100644
--- a/compose/ui/ui-text/src/androidMain/kotlin/androidx/compose/ui/text/platform/URLSpanCache.android.kt
+++ b/compose/ui/ui-text/src/androidMain/kotlin/androidx/compose/ui/text/platform/URLSpanCache.android.kt
@@ -16,10 +16,17 @@
 
 package androidx.compose.ui.text.platform
 
+import android.text.style.ClickableSpan
 import android.text.style.URLSpan
+import android.view.View
+import androidx.annotation.RestrictTo
+import androidx.collection.LongObjectMap
+import androidx.compose.ui.text.AnnotatedString
 import androidx.compose.ui.text.ExperimentalTextApi
 import androidx.compose.ui.text.InternalTextApi
+import androidx.compose.ui.text.LinkAnnotation
 import androidx.compose.ui.text.UrlAnnotation
+import androidx.compose.ui.util.packInts
 import java.util.WeakHashMap
 
 /**
@@ -37,10 +44,52 @@
 @Suppress("AcronymName")
 @OptIn(ExperimentalTextApi::class)
 @InternalTextApi
+@RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
 class URLSpanCache {
     private val spansByAnnotation = WeakHashMap<UrlAnnotation, URLSpan>()
+    private val urlSpansByAnnotation =
+        WeakHashMap<AnnotatedString.Range<LinkAnnotation.Url>, URLSpan>()
+    private val clickableSpansByAnnotation =
+        WeakHashMap<AnnotatedString.Range<LinkAnnotation>, Pair<ComposeClickableSpan, Int>>()
 
     @Suppress("AcronymName")
     fun toURLSpan(urlAnnotation: UrlAnnotation): URLSpan =
         spansByAnnotation.getOrPut(urlAnnotation) { URLSpan(urlAnnotation.url) }
+
+    @Suppress("AcronymName")
+    fun toURLSpan(urlRange: AnnotatedString.Range<LinkAnnotation.Url>): URLSpan =
+        urlSpansByAnnotation.getOrPut(urlRange) { URLSpan(urlRange.item.url) }
+
+    /**
+     * This method takes a [linkRange] which is an annotation that occupies range in Compose text
+     * and converts it into a ClickableSpan passing the corresponding [linkActions] to the
+     * ClickableSpan's onClick method.
+     * We use [accessibilityNodeId] to invalidate cache entry for cases when the original Compose text was
+     * disposed and so the corresponding link actions are not valid anymore
+     */
+    fun toClickableSpan(
+        linkRange: AnnotatedString.Range<LinkAnnotation>,
+        linkActions: LongObjectMap<() -> Unit>,
+        accessibilityNodeId: Int
+    ): ClickableSpan? {
+        val spanWithNodeId = clickableSpansByAnnotation[linkRange]
+
+        return if (spanWithNodeId == null || spanWithNodeId.second != accessibilityNodeId) {
+            // either clickable span hasn't been created yet or the cache needs invalidation for a
+            // given annotation
+            linkActions[packInts(linkRange.start, linkRange.end)]?.let { action ->
+                ComposeClickableSpan(action).also { newSpan ->
+                    clickableSpansByAnnotation[linkRange] = Pair(newSpan, accessibilityNodeId)
+                }
+            }
+        } else {
+            spanWithNodeId.first
+        }
+    }
+}
+
+private class ComposeClickableSpan(private val linkAction: () -> Unit) : ClickableSpan() {
+    override fun onClick(widget: View) {
+        linkAction()
+    }
 }
diff --git a/compose/ui/ui-unit/src/commonMain/kotlin/androidx/compose/ui/unit/Constraints.kt b/compose/ui/ui-unit/src/commonMain/kotlin/androidx/compose/ui/unit/Constraints.kt
index 4eecfad..ceda641 100644
--- a/compose/ui/ui-unit/src/commonMain/kotlin/androidx/compose/ui/unit/Constraints.kt
+++ b/compose/ui/ui-unit/src/commonMain/kotlin/androidx/compose/ui/unit/Constraints.kt
@@ -16,9 +16,9 @@
 
 package androidx.compose.ui.unit
 
-import androidx.collection.IntIntPair
 import androidx.compose.runtime.Immutable
 import androidx.compose.runtime.Stable
+import androidx.compose.ui.unit.Constraints.Companion.Infinity
 import kotlin.math.min
 
 /**
@@ -223,84 +223,6 @@
         const val Infinity = Int.MAX_VALUE
 
         /**
-         * The bit distribution when the focus of the bits should be on the width, but only
-         * a minimal difference in focus.
-         *
-         * 16 bits assigned to width, 15 bits assigned to height.
-         */
-        private const val MinFocusWidth = 0x02L
-
-        /**
-         * The bit distribution when the focus of the bits should be on the width, and a
-         * maximal number of bits assigned to the width.
-         *
-         * 18 bits assigned to width, 13 bits assigned to height.
-         */
-        private const val MaxFocusWidth = 0x03L
-
-        /**
-         * The bit distribution when the focus of the bits should be on the height, but only
-         * a minimal difference in focus.
-         *
-         * 15 bits assigned to width, 16 bits assigned to height.
-         */
-        private const val MinFocusHeight = 0x01L
-
-        /**
-         * The bit distribution when the focus of the bits should be on the height, and a
-         * a maximal number of bits assigned to the height.
-         *
-         * 13 bits assigned to width, 18 bits assigned to height.
-         */
-        private const val MaxFocusHeight = 0x00L
-
-        /**
-         * The mask to retrieve the focus ([MinFocusWidth], [MaxFocusWidth],
-         * [MinFocusHeight], [MaxFocusHeight]).
-         */
-        private const val FocusMask = 0x03L
-
-        /**
-         * The number of bits used for the focused dimension when there is minimal focus.
-         */
-        private const val MinFocusBits = 16
-
-        /**
-         * The mask to use for the focused dimension when there is minimal focus.
-         */
-        private const val MinFocusMask = 0xFFFF // 64K (16 bits)
-
-        /**
-         * The number of bits used for the non-focused dimension when there is minimal focus.
-         */
-        private const val MinNonFocusBits = 15
-
-        /**
-         * The mask to use for the non-focused dimension when there is minimal focus.
-         */
-        private const val MinNonFocusMask = 0x7FFF // 32K (15 bits)
-
-        /**
-         * The number of bits to use for the focused dimension when there is maximal focus.
-         */
-        private const val MaxFocusBits = 18
-
-        /**
-         * The mask to use for the focused dimension when there is maximal focus.
-         */
-        private const val MaxFocusMask = 0x3FFFF // 256K (18 bits)
-
-        /**
-         * The number of bits to use for the non-focused dimension when there is maximal focus.
-         */
-        private const val MaxNonFocusBits = 13
-
-        /**
-         * The mask to use for the non-focused dimension when there is maximal focus.
-         */
-        private const val MaxNonFocusMask = 0x1FFF // 8K (13 bits)
-
-        /**
          * Creates constraints for fixed size in both dimensions.
          */
         @Stable
@@ -374,95 +296,197 @@
             prioritizeWidth: Boolean = true
         ): Constraints {
             if (prioritizeWidth) {
-                val (minW, maxW) = calculatePriorityDimension(minWidth, maxWidth)
+                val minW = min(minWidth, MaxFocusMask - 1)
+                val maxW = if (maxWidth == Infinity) {
+                    Infinity
+                } else {
+                    min(maxWidth, MaxFocusMask - 1)
+                }
                 val consumed = if (maxW == Infinity) minW else maxW
-                val bitsUsed = bitsNeedForSize(consumed)
-                val (minH, maxH) = calculateOtherDimension(minHeight, maxHeight, bitsUsed)
+                val maxAllowed = maxAllowedForSize(consumed)
+                val maxH =
+                    if (maxHeight == Infinity) Infinity else min(maxAllowed, maxHeight)
+                val minH = min(maxAllowed, minHeight)
                 return Constraints(minW, maxW, minH, maxH)
             }
-            val (minH, maxH) = calculatePriorityDimension(minHeight, maxHeight)
-            val consumed = if (maxH == Infinity) minH else maxH
-            val bitsUsed = bitsNeedForSize(consumed)
-            val (minW, maxW) = calculateOtherDimension(minWidth, maxWidth, bitsUsed)
-            return Constraints(minW, maxW, minH, maxH)
-        }
 
-        private fun calculatePriorityDimension(minValue: Int, maxValue: Int): IntIntPair {
-            val newMinValue = min(minValue, MaxFocusMask - 1)
-            val newMaxValue = if (maxValue == Infinity) {
+            val minH = min(minHeight, MaxFocusMask - 1)
+            val maxH = if (maxHeight == Infinity) {
                 Infinity
             } else {
-                min(maxValue, MaxFocusMask - 1)
+                min(maxHeight, MaxFocusMask - 1)
             }
-            return IntIntPair(newMinValue, newMaxValue)
+            val consumed = if (maxH == Infinity) minH else maxH
+            val maxAllowed = maxAllowedForSize(consumed)
+            val maxW = if (maxWidth == Infinity) Infinity else min(maxAllowed, maxWidth)
+            val minW = min(maxAllowed, minWidth)
+            return Constraints(minW, maxW, minH, maxH)
         }
+    }
+}
 
-        private fun calculateOtherDimension(
-            minValue: Int,
-            maxValue: Int,
-            bitsUsed: Int
-        ): IntIntPair {
-            val allowedBits = 31 - bitsUsed
-            val maxAllowed = (1 shl allowedBits) - 2
-            val newMaxValue = if (maxValue == Infinity) Infinity else min(maxAllowed, maxValue)
-            val newMinValue = min(maxAllowed, minValue)
-            return IntIntPair(newMinValue, newMaxValue)
-        }
+// Redefinition of Constraints.Infinity to bypass the companion object
+private const val Infinity = Int.MAX_VALUE
 
-        /**
-         * Creates a [Constraints], only checking that the values fit in the packed Long.
-         */
-        internal fun createConstraints(
-            minWidth: Int,
-            maxWidth: Int,
-            minHeight: Int,
-            maxHeight: Int
-        ): Constraints {
-            val heightVal = if (maxHeight == Infinity) minHeight else maxHeight
-            val heightBits = bitsNeedForSize(heightVal)
+/**
+ * The bit distribution when the focus of the bits should be on the width, but only
+ * a minimal difference in focus.
+ *
+ * 16 bits assigned to width, 15 bits assigned to height.
+ */
+private const val MinFocusWidth = 0x02
 
-            val widthVal = if (maxWidth == Infinity) minWidth else maxWidth
-            val widthBits = bitsNeedForSize(widthVal)
+/**
+ * The bit distribution when the focus of the bits should be on the width, and a
+ * maximal number of bits assigned to the width.
+ *
+ * 18 bits assigned to width, 13 bits assigned to height.
+ */
+private const val MaxFocusWidth = 0x03
 
-            if (widthBits + heightBits > 31) {
-                throwIllegalArgumentException(
-                    "Can't represent a width of $widthVal and height of $heightVal in Constraints"
-                )
-            }
+/**
+ * The bit distribution when the focus of the bits should be on the height, but only
+ * a minimal difference in focus.
+ *
+ * 15 bits assigned to width, 16 bits assigned to height.
+ */
+private const val MinFocusHeight = 0x01
 
-            val focus = when (widthBits) {
-                MinNonFocusBits -> MinFocusHeight
-                MinFocusBits -> MinFocusWidth
-                MaxNonFocusBits -> MaxFocusHeight
-                MaxFocusBits -> MaxFocusWidth
-                else -> throw IllegalStateException("Should only have the provided constants.")
-            }
+/**
+ * The bit distribution when the focus of the bits should be on the height, and a
+ * a maximal number of bits assigned to the height.
+ *
+ * 13 bits assigned to width, 18 bits assigned to height.
+ */
+private const val MaxFocusHeight = 0x00
 
-            val maxWidthValue = if (maxWidth == Infinity) 0 else maxWidth + 1
-            val maxHeightValue = if (maxHeight == Infinity) 0 else maxHeight + 1
+/**
+ * The mask to retrieve the focus ([MinFocusWidth], [MaxFocusWidth],
+ * [MinFocusHeight], [MaxFocusHeight]).
+ */
+private const val FocusMask = 0x03L
 
-            val minHeightOffset = minHeightOffsets(indexToBitOffset(focus.toInt()))
-            val maxHeightOffset = minHeightOffset + 31
+/**
+ * The number of bits used for the focused dimension when there is minimal focus.
+ */
+private const val MinFocusBits = 16
+private const val MaxAllowedForMinFocusBits = (1 shl (31 - MinFocusBits)) - 2
 
-            val value = focus or
-                (minWidth.toLong() shl 2) or
-                (maxWidthValue.toLong() shl 33) or
-                (minHeight.toLong() shl minHeightOffset) or
-                (maxHeightValue.toLong() shl maxHeightOffset)
-            return Constraints(value)
-        }
+/**
+ * The mask to use for the focused dimension when there is minimal focus.
+ */
+private const val MinFocusMask = 0xFFFF // 64K (16 bits)
 
-        private fun bitsNeedForSize(size: Int): Int {
-            return when {
-                size < MaxNonFocusMask -> MaxNonFocusBits
-                size < MinNonFocusMask -> MinNonFocusBits
-                size < MinFocusMask -> MinFocusBits
-                size < MaxFocusMask -> MaxFocusBits
-                else -> throw IllegalArgumentException(
-                    "Can't represent a size of $size in Constraints"
-                )
-            }
-        }
+/**
+ * The number of bits used for the non-focused dimension when there is minimal focus.
+ */
+private const val MinNonFocusBits = 15
+private const val MaxAllowedForMinNonFocusBits = (1 shl (31 - MinNonFocusBits)) - 2
+
+/**
+ * The mask to use for the non-focused dimension when there is minimal focus.
+ */
+private const val MinNonFocusMask = 0x7FFF // 32K (15 bits)
+
+/**
+ * The number of bits to use for the focused dimension when there is maximal focus.
+ */
+private const val MaxFocusBits = 18
+private const val MaxAllowedForMaxFocusBits = (1 shl (31 - MaxFocusBits)) - 2
+
+/**
+ * The mask to use for the focused dimension when there is maximal focus.
+ */
+private const val MaxFocusMask = 0x3FFFF // 256K (18 bits)
+
+/**
+ * The number of bits to use for the non-focused dimension when there is maximal focus.
+ */
+private const val MaxNonFocusBits = 13
+private const val MaxAllowedForMaxNonFocusBits = (1 shl (31 - MaxNonFocusBits)) - 2
+
+/**
+ * The mask to use for the non-focused dimension when there is maximal focus.
+ */
+private const val MaxNonFocusMask = 0x1FFF // 8K (13 bits)
+
+// Wrap those throws in functions to avoid inlining the string building at the call sites
+private fun invalidConstraint(widthVal: Int, heightVal: Int) {
+    throw IllegalArgumentException(
+        "Can't represent a width of $widthVal and height of $heightVal in Constraints"
+    )
+}
+
+private fun invalidSize(size: Int): Nothing {
+    throw IllegalArgumentException(
+        "Can't represent a size of $size in Constraints"
+    )
+}
+
+/**
+ * Creates a [Constraints], only checking that the values fit in the packed Long.
+ */
+internal fun createConstraints(
+    minWidth: Int,
+    maxWidth: Int,
+    minHeight: Int,
+    maxHeight: Int
+): Constraints {
+    val heightVal = if (maxHeight == Infinity) minHeight else maxHeight
+    val heightBits = bitsNeedForSizeUnchecked(heightVal)
+
+    val widthVal = if (maxWidth == Infinity) minWidth else maxWidth
+    val widthBits = bitsNeedForSizeUnchecked(widthVal)
+
+    if (widthBits + heightBits > 31) {
+        invalidConstraint(widthVal, heightVal)
+    }
+
+    // Same as if (maxWidth == Infinity) 0 else maxWidth + 1 but branchless
+    // in DEX and saves 2 instructions on aarch64. Relies on integer overflow
+    // since Infinity == Int.MAX_VALUE
+    var maxWidthValue = maxWidth + 1
+    maxWidthValue = maxWidthValue and (maxWidthValue shr 31).inv()
+
+    var maxHeightValue = maxHeight + 1
+    maxHeightValue = maxHeightValue and (maxHeightValue shr 31).inv()
+
+    val focus = when (widthBits) {
+        MinNonFocusBits -> MinFocusHeight
+        MinFocusBits -> MinFocusWidth
+        MaxNonFocusBits -> MaxFocusHeight
+        MaxFocusBits -> MaxFocusWidth
+        else -> 0x00 // can't happen, widthBits is computed from bitsNeedForSizeUnchecked()
+    }
+
+    val minHeightOffset = minHeightOffsets(indexToBitOffset(focus))
+    val maxHeightOffset = minHeightOffset + 31
+
+    val value = focus.toLong() or
+        (minWidth.toLong() shl 2) or
+        (maxWidthValue.toLong() shl 33) or
+        (minHeight.toLong() shl minHeightOffset) or
+        (maxHeightValue.toLong() shl maxHeightOffset)
+    return Constraints(value)
+}
+
+private fun bitsNeedForSizeUnchecked(size: Int): Int {
+    return when {
+        size < MaxNonFocusMask -> MaxNonFocusBits
+        size < MinNonFocusMask -> MinNonFocusBits
+        size < MinFocusMask -> MinFocusBits
+        size < MaxFocusMask -> MaxFocusBits
+        else -> 255
+    }
+}
+
+private fun maxAllowedForSize(size: Int): Int {
+    return when {
+        size < MaxNonFocusMask -> MaxAllowedForMaxNonFocusBits
+        size < MinNonFocusMask -> MaxAllowedForMinNonFocusBits
+        size < MinFocusMask -> MaxAllowedForMinFocusBits
+        size < MaxFocusMask -> MaxAllowedForMaxFocusBits
+        else -> invalidSize(size)
     }
 }
 
@@ -487,7 +511,7 @@
     requirePrecondition(minWidth >= 0 && minHeight >= 0) {
         "minWidth($minWidth) and minHeight($minHeight) must be >= 0"
     }
-    return Constraints.createConstraints(minWidth, maxWidth, minHeight, maxHeight)
+    return createConstraints(minWidth, maxWidth, minHeight, maxHeight)
 }
 
 /**
diff --git a/compose/ui/ui-unit/src/commonMain/kotlin/androidx/compose/ui/unit/InlineClassHelper.kt b/compose/ui/ui-unit/src/commonMain/kotlin/androidx/compose/ui/unit/InlineClassHelper.kt
index 42101b0..4d5cfec 100644
--- a/compose/ui/ui-unit/src/commonMain/kotlin/androidx/compose/ui/unit/InlineClassHelper.kt
+++ b/compose/ui/ui-unit/src/commonMain/kotlin/androidx/compose/ui/unit/InlineClassHelper.kt
@@ -29,6 +29,10 @@
     throw IllegalArgumentException(message)
 }
 
+internal fun throwIllegalArgumentExceptionNoReturn(message: String): Nothing {
+    throw IllegalArgumentException(message)
+}
+
 // Like Kotlin's require() but without the .toString() call
 @Suppress("BanInlineOptIn") // same opt-in as using Kotlin's require()
 @OptIn(ExperimentalContracts::class)
diff --git a/compose/ui/ui/api/current.txt b/compose/ui/ui/api/current.txt
index ea780c4..e647af0 100644
--- a/compose/ui/ui/api/current.txt
+++ b/compose/ui/ui/api/current.txt
@@ -3637,7 +3637,7 @@
 
   @androidx.compose.runtime.Immutable public final class PopupProperties {
     ctor public PopupProperties(optional boolean focusable, optional boolean dismissOnBackPress, optional boolean dismissOnClickOutside, optional androidx.compose.ui.window.SecureFlagPolicy securePolicy, optional boolean excludeFromSystemGesture, optional boolean clippingEnabled);
-    ctor @SuppressCompatibility @androidx.compose.ui.ExperimentalComposeUiApi public PopupProperties(optional boolean focusable, optional boolean dismissOnBackPress, optional boolean dismissOnClickOutside, optional androidx.compose.ui.window.SecureFlagPolicy securePolicy, optional boolean excludeFromSystemGesture, optional boolean clippingEnabled, optional boolean usePlatformDefaultWidth);
+    ctor public PopupProperties(optional boolean focusable, optional boolean dismissOnBackPress, optional boolean dismissOnClickOutside, optional androidx.compose.ui.window.SecureFlagPolicy securePolicy, optional boolean excludeFromSystemGesture, optional boolean clippingEnabled, optional boolean usePlatformDefaultWidth);
     ctor public PopupProperties(optional boolean focusable, optional boolean dismissOnBackPress, optional boolean dismissOnClickOutside, optional boolean clippingEnabled);
     method public boolean getClippingEnabled();
     method public boolean getDismissOnBackPress();
diff --git a/compose/ui/ui/api/restricted_current.txt b/compose/ui/ui/api/restricted_current.txt
index d9d8a86..df37d14 100644
--- a/compose/ui/ui/api/restricted_current.txt
+++ b/compose/ui/ui/api/restricted_current.txt
@@ -3697,7 +3697,7 @@
 
   @androidx.compose.runtime.Immutable public final class PopupProperties {
     ctor public PopupProperties(optional boolean focusable, optional boolean dismissOnBackPress, optional boolean dismissOnClickOutside, optional androidx.compose.ui.window.SecureFlagPolicy securePolicy, optional boolean excludeFromSystemGesture, optional boolean clippingEnabled);
-    ctor @SuppressCompatibility @androidx.compose.ui.ExperimentalComposeUiApi public PopupProperties(optional boolean focusable, optional boolean dismissOnBackPress, optional boolean dismissOnClickOutside, optional androidx.compose.ui.window.SecureFlagPolicy securePolicy, optional boolean excludeFromSystemGesture, optional boolean clippingEnabled, optional boolean usePlatformDefaultWidth);
+    ctor public PopupProperties(optional boolean focusable, optional boolean dismissOnBackPress, optional boolean dismissOnClickOutside, optional androidx.compose.ui.window.SecureFlagPolicy securePolicy, optional boolean excludeFromSystemGesture, optional boolean clippingEnabled, optional boolean usePlatformDefaultWidth);
     ctor public PopupProperties(optional boolean focusable, optional boolean dismissOnBackPress, optional boolean dismissOnClickOutside, optional boolean clippingEnabled);
     method public boolean getClippingEnabled();
     method public boolean getDismissOnBackPress();
diff --git a/compose/ui/ui/integration-tests/ui-demos/src/main/AndroidManifest.xml b/compose/ui/ui/integration-tests/ui-demos/src/main/AndroidManifest.xml
index 81be1d2..68a48d8 100644
--- a/compose/ui/ui/integration-tests/ui-demos/src/main/AndroidManifest.xml
+++ b/compose/ui/ui/integration-tests/ui-demos/src/main/AndroidManifest.xml
@@ -73,5 +73,15 @@
             layoutDirection|locale|mcc|mnc|navigation|orientation|screenLayout|screenSize|
             smallestScreenSize|touchscreen|uiMode"
             android:label="Painter Resources Demo" />
+
+        <activity
+            android:name=".SimpleChatActivity"
+            android:exported="true" >
+            <intent-filter>
+                <action android:name=".SIMPLECOMPOSECHAT"/>
+                <action android:name="android.intent.action.MAIN" />
+                <category android:name="android.intent.category.LAUNCHER" />
+            </intent-filter>
+        </activity>
     </application>
 </manifest>
\ No newline at end of file
diff --git a/compose/ui/ui/integration-tests/ui-demos/src/main/java/androidx/compose/ui/demos/SimpleChatActivity.kt b/compose/ui/ui/integration-tests/ui-demos/src/main/java/androidx/compose/ui/demos/SimpleChatActivity.kt
new file mode 100644
index 0000000..2e10cef
--- /dev/null
+++ b/compose/ui/ui/integration-tests/ui-demos/src/main/java/androidx/compose/ui/demos/SimpleChatActivity.kt
@@ -0,0 +1,185 @@
+/*
+ * Copyright 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.compose.ui.demos
+
+import android.annotation.SuppressLint
+import android.content.LocusId
+import android.os.Build
+import android.os.Bundle
+import androidx.activity.ComponentActivity
+import androidx.activity.compose.setContent
+import androidx.compose.foundation.layout.Arrangement
+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.fillMaxSize
+import androidx.compose.foundation.layout.height
+import androidx.compose.foundation.layout.padding
+import androidx.compose.foundation.lazy.LazyColumn
+import androidx.compose.foundation.lazy.LazyListState
+import androidx.compose.foundation.lazy.items
+import androidx.compose.foundation.lazy.rememberLazyListState
+import androidx.compose.foundation.text.selection.SelectionContainer
+import androidx.compose.material.Button
+import androidx.compose.material.MaterialTheme
+import androidx.compose.material.Scaffold
+import androidx.compose.material.Text
+import androidx.compose.material.TextField
+import androidx.compose.material.TopAppBar
+import androidx.compose.runtime.Composable
+import androidx.compose.runtime.getValue
+import androidx.compose.runtime.mutableStateListOf
+import androidx.compose.runtime.mutableStateOf
+import androidx.compose.runtime.remember
+import androidx.compose.runtime.rememberCoroutineScope
+import androidx.compose.runtime.setValue
+import androidx.compose.ui.ExperimentalComposeUiApi
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.platform.testTag
+import androidx.compose.ui.semantics.semantics
+import androidx.compose.ui.semantics.testTagsAsResourceId
+import androidx.compose.ui.unit.dp
+import androidx.compose.ui.unit.sp
+import kotlinx.coroutines.launch
+
+@SuppressLint("ClassVerificationFailure")
+class SimpleChatActivity : ComponentActivity() {
+    override fun onCreate(savedInstanceState: Bundle?) {
+        super.onCreate(savedInstanceState)
+
+        // in ActivityCompat the `setLocusContext` method is a no-op on lower version so we just
+        // check the version here instead of using the compat version of activity
+        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) {
+            // Always set locus id as 1
+            this.setLocusContext(LocusId("1"), null)
+        }
+        setContent { MaterialTheme { SimpleChatPage() } }
+    }
+}
+
+private data class Message(val content: String, val isReceived: Boolean = true)
+
+@SuppressLint("NullAnnotationGroup")
+@OptIn(ExperimentalComposeUiApi::class)
+@Composable
+private fun SimpleChatPage() {
+    val messages = remember { mutableStateListOf<Message>() }
+    val listState = rememberLazyListState()
+    val coroutineScope = rememberCoroutineScope()
+    Scaffold(
+        modifier = Modifier.semantics { testTagsAsResourceId = true },
+        topBar = {
+            TopAppBar(
+                elevation = 4.dp,
+                title = {
+                    Text(
+                        "Conversation Page",
+                        modifier = Modifier.testTag("tool_bar_name"),
+                        fontSize = 30.sp
+                    )
+                }
+            )
+        },
+        bottomBar = {
+            MessageUpdater(
+                onMessageAdded = { message, isReceived ->
+                    messages.add(Message(message, isReceived = isReceived))
+                    coroutineScope.launch { listState.animateScrollToItem(messages.size) }
+                }
+            )
+        }
+    ) { contentPadding ->
+        Box(modifier = Modifier.padding(contentPadding)) {
+            // testTagsAsResourceId and testTag is for compose to map testTag to resource-id.
+            // https://developer.android.com/jetpack/compose/testing#uiautomator-interop
+            SelectionContainer() {
+                Column {
+                    Conversation(messages, listState)
+                }
+            }
+        }
+    }
+}
+
+@Composable
+private fun Conversation(messages: List<Message>, state: LazyListState) {
+    LazyColumn(
+        modifier = Modifier
+            .testTag("messages")
+            .fillMaxSize(),
+        state = state,
+        verticalArrangement = Arrangement.Bottom,
+    ) {
+        items(messages) { MessageCard(it) }
+    }
+}
+
+@Composable
+private fun MessageCard(message: Message) {
+    Row {
+        if (!message.isReceived) {
+            Spacer(modifier = Modifier.weight(1.0f))
+        }
+        Column {
+            Text(
+                message.content,
+                fontSize = 20.sp,
+                modifier = Modifier.testTag(
+                    if (message.isReceived) "message_received" else "message_sent"
+                )
+            )
+            Spacer(modifier = Modifier.height(16.dp))
+        }
+    }
+}
+
+@Composable
+private fun MessageUpdater(onMessageAdded: (message: String, isReceived: Boolean) -> Unit) {
+    Row {
+        var text by remember { mutableStateOf("") }
+
+        TextField(
+            modifier = Modifier.weight(1.0f),
+            value = text,
+            onValueChange = { text = it },
+            placeholder = { Text("Input message here") }
+        )
+
+        Button(
+            onClick = {
+                if (text.isNotEmpty()) {
+                    onMessageAdded(text, true)
+                    text = ""
+                }
+            }
+        ) {
+            Text("Receive")
+        }
+
+        Button(
+            onClick = {
+                if (text.isNotEmpty()) {
+                    onMessageAdded(text, false)
+                    text = ""
+                }
+            }
+        ) {
+            Text("Send")
+        }
+    }
+}
diff --git a/compose/ui/ui/integration-tests/ui-demos/src/main/java/androidx/compose/ui/demos/UiDemos.kt b/compose/ui/ui/integration-tests/ui-demos/src/main/java/androidx/compose/ui/demos/UiDemos.kt
index 2bbfa78..e1461ae 100644
--- a/compose/ui/ui/integration-tests/ui-demos/src/main/java/androidx/compose/ui/demos/UiDemos.kt
+++ b/compose/ui/ui/integration-tests/ui-demos/src/main/java/androidx/compose/ui/demos/UiDemos.kt
@@ -275,6 +275,7 @@
         RecyclerViewDemos,
         AccessibilityDemos,
         ComposableDemo("Screen coordinates") { ScreenCoordinatesDemo(it) },
-        ComposableDemo("Clipboard") { ClipboardDemo() }
+        ComposableDemo("Clipboard") { ClipboardDemo() },
+        ActivityDemo("Simple chat", SimpleChatActivity::class)
     )
 )
diff --git a/compose/ui/ui/src/androidInstrumentedTest/kotlin/androidx/compose/ui/AndroidAccessibilityTest.kt b/compose/ui/ui/src/androidInstrumentedTest/kotlin/androidx/compose/ui/AndroidAccessibilityTest.kt
index e9f3640..d9e4578 100644
--- a/compose/ui/ui/src/androidInstrumentedTest/kotlin/androidx/compose/ui/AndroidAccessibilityTest.kt
+++ b/compose/ui/ui/src/androidInstrumentedTest/kotlin/androidx/compose/ui/AndroidAccessibilityTest.kt
@@ -80,7 +80,9 @@
 import androidx.compose.material.DrawerValue
 import androidx.compose.material.DropdownMenu
 import androidx.compose.material.DropdownMenuItem
+import androidx.compose.material.ExperimentalMaterialApi
 import androidx.compose.material.FabPosition
+import androidx.compose.material.FilterChip
 import androidx.compose.material.FloatingActionButton
 import androidx.compose.material.Icon
 import androidx.compose.material.IconButton
@@ -236,6 +238,7 @@
 import org.mockito.kotlin.verify
 
 @LargeTest
+@OptIn(ExperimentalMaterialApi::class)
 @RunWith(AndroidJUnit4::class)
 class AndroidAccessibilityTest {
     @get:Rule
@@ -459,13 +462,14 @@
             with(AccessibilityNodeInfoCompat.wrap(info)) {
                 assertThat(className).isEqualTo("android.view.View")
                 assertThat(stateDescription).isEqualTo("Selected")
-                assertThat(isClickable).isFalse()
+                assertThat(isClickable).isTrue()
                 assertThat(isCheckable).isTrue()
                 assertThat(isVisibleToUser).isTrue()
                 assertThat(actionList)
                     .containsExactly(
                         AccessibilityActionCompat(ACTION_ACCESSIBILITY_FOCUS, null),
                         AccessibilityActionCompat(ACTION_FOCUS, null),
+                        AccessibilityActionCompat(ACTION_CLICK, null),
                     )
             }
         }
@@ -505,6 +509,67 @@
     }
 
     @Test
+    fun testCreateAccessibilityNodeInfo_forRadioButton() {
+        // Arrange.
+        setContent {
+            Box(
+                Modifier
+                    .selectable(selected = true, onClick = {}, role = Role.RadioButton)
+                    .testTag(tag)) {
+                BasicText("Text")
+            }
+        }
+        val virtualId = rule.onNodeWithTag(tag).semanticsId
+
+        // Act.
+        val info = rule.runOnIdle { createAccessibilityNodeInfo(virtualId) }
+
+        // Assert.
+        rule.runOnIdle {
+            with(AccessibilityNodeInfoCompat.wrap(info)) {
+                assertThat(className).isEqualTo("android.view.View")
+                assertThat(isClickable).isFalse()
+                assertThat(isVisibleToUser).isTrue()
+                assertThat(isChecked).isTrue()
+                assertThat(actionList)
+                    .containsExactly(
+                        AccessibilityActionCompat(ACTION_ACCESSIBILITY_FOCUS, null),
+                        AccessibilityActionCompat(ACTION_FOCUS, null),
+                    )
+            }
+        }
+    }
+
+    @Test
+    fun testCreateAccessibilityNodeInfo_forFilterButton() {
+        // Arrange.
+        setContent {
+            FilterChip(selected = true, onClick = {}, modifier = Modifier.testTag(tag)) {
+                Text("Filter chip")
+            }
+        }
+        val virtualId = rule.onNodeWithTag(tag).semanticsId
+
+        // Act.
+        val info = rule.runOnIdle { createAccessibilityNodeInfo(virtualId) }
+
+        // Assert.
+        rule.runOnIdle {
+            with(AccessibilityNodeInfoCompat.wrap(info)) {
+                // We don't check for a CheckBox role since the role is found in a fake descendant.
+                assertThat(stateDescription).isEqualTo("Selected")
+                assertThat(isClickable).isTrue()
+                assertThat(isCheckable).isTrue()
+                assertThat(isChecked).isTrue()
+                assertThat(isVisibleToUser).isTrue()
+                assertThat(actionList)
+                    .contains(
+                        AccessibilityActionCompat(ACTION_CLICK, null),
+                    )
+            }
+        }
+    }
+    @Test
     fun testCreateAccessibilityNodeInfo_progressIndicator_determinate() {
         // Arrange.
         setContent {
diff --git a/compose/ui/ui/src/androidInstrumentedTest/kotlin/androidx/compose/ui/AndroidComposeViewAccessibilityDelegateCompatTest.kt b/compose/ui/ui/src/androidInstrumentedTest/kotlin/androidx/compose/ui/AndroidComposeViewAccessibilityDelegateCompatTest.kt
index 015999c..6623a0f 100644
--- a/compose/ui/ui/src/androidInstrumentedTest/kotlin/androidx/compose/ui/AndroidComposeViewAccessibilityDelegateCompatTest.kt
+++ b/compose/ui/ui/src/androidInstrumentedTest/kotlin/androidx/compose/ui/AndroidComposeViewAccessibilityDelegateCompatTest.kt
@@ -1042,6 +1042,7 @@
         rule.runOnIdle { assertThat(info.childCount).isEqualTo(1) }
     }
 
+    @SdkSuppress(maxSdkVersion = 33) // b/321824038
     @Test
     fun testUncoveredNodes_zeroBoundsRoot_included() {
         // Arrange.
diff --git a/compose/ui/ui/src/androidInstrumentedTest/kotlin/androidx/compose/ui/accessibility/ScrollingTest.kt b/compose/ui/ui/src/androidInstrumentedTest/kotlin/androidx/compose/ui/accessibility/ScrollingTest.kt
index 04494ed..b7386f0 100644
--- a/compose/ui/ui/src/androidInstrumentedTest/kotlin/androidx/compose/ui/accessibility/ScrollingTest.kt
+++ b/compose/ui/ui/src/androidInstrumentedTest/kotlin/androidx/compose/ui/accessibility/ScrollingTest.kt
@@ -61,6 +61,7 @@
 import androidx.core.view.accessibility.AccessibilityNodeInfoCompat.ACTION_ACCESSIBILITY_FOCUS
 import androidx.test.ext.junit.runners.AndroidJUnit4
 import androidx.test.filters.MediumTest
+import androidx.test.filters.SdkSuppress
 import com.google.common.truth.Correspondence
 import com.google.common.truth.Truth.assertThat
 import org.junit.Rule
@@ -78,6 +79,7 @@
     private val dispatchedAccessibilityEvents = mutableListOf<AccessibilityEvent>()
     private val accessibilityEventLoopIntervalMs = 100L
 
+    @SdkSuppress(maxSdkVersion = 33) // b/322354981
     @Test
     fun sendScrollEvent_byStateObservation_horizontal() {
         // Arrange.
diff --git a/compose/ui/ui/src/androidInstrumentedTest/kotlin/androidx/compose/ui/input/pointer/ClipPointerInputTest.kt b/compose/ui/ui/src/androidInstrumentedTest/kotlin/androidx/compose/ui/input/pointer/ClipPointerInputTest.kt
index 0777bc1..a845d2f 100644
--- a/compose/ui/ui/src/androidInstrumentedTest/kotlin/androidx/compose/ui/input/pointer/ClipPointerInputTest.kt
+++ b/compose/ui/ui/src/androidInstrumentedTest/kotlin/androidx/compose/ui/input/pointer/ClipPointerInputTest.kt
@@ -44,6 +44,7 @@
 import androidx.compose.ui.unit.IntSize
 import androidx.compose.ui.unit.LayoutDirection
 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
@@ -93,6 +94,7 @@
      * Because clipToBounds is being used on the parent LayoutNode, only the 4 touches inside the
      * parent LayoutNode should hit.
      */
+    @SdkSuppress(maxSdkVersion = 33) // b/321823104
     @Test
     fun clipToBounds_childrenOffsetViaLayout_onlyCorrectPointersHit() {
 
@@ -220,6 +222,7 @@
      * Because clipToBounds is being used on the parent LayoutNode, only the 4 touches inside the
      * parent LayoutNode should hit.
      */
+    @SdkSuppress(maxSdkVersion = 33) // b/321823104
     @Test
     fun clipToBounds_childrenOffsetViaModifier_onlyCorrectPointersHit() {
 
@@ -327,6 +330,7 @@
      * This test creates a layout clipped to a rounded rectangle shape (circle).
      * We'll touch in and out of the rounded area.
      */
+    @SdkSuppress(maxSdkVersion = 33) // b/321823104
     @Test
     fun clip_roundedRect() {
         pokeAroundCircle(RoundedCornerShape(50))
@@ -337,6 +341,7 @@
      * corners are defined as larger than the side length
      * We'll touch in and out of the rounded area.
      */
+    @SdkSuppress(maxSdkVersion = 33) // b/321823104
     @Test
     fun clip_roundedRectLargeCorner() {
         pokeAroundCircle(RoundedCornerShape(1.1f))
@@ -346,6 +351,7 @@
      * This test creates a layout clipped to a generic shape (circle).
      * We'll touch in and out of the rounded area.
      */
+    @SdkSuppress(maxSdkVersion = 33) // b/321823104
     @Test
     fun clip_genericShape() {
         pokeAroundCircle(
@@ -446,6 +452,7 @@
      * This creates a clipped rectangle that is smaller than the bounds and ensures that only the
      * clipped area receives touches
      */
+    @SdkSuppress(maxSdkVersion = 33) // b/321823104
     @Test
     fun clip_smallRect() {
         val rectangleShape: Shape = object : Shape {
diff --git a/compose/ui/ui/src/androidInstrumentedTest/kotlin/androidx/compose/ui/platform/AndroidClipboardManagerTest.kt b/compose/ui/ui/src/androidInstrumentedTest/kotlin/androidx/compose/ui/platform/AndroidClipboardManagerTest.kt
index df0dbf1..0ffc44f 100644
--- a/compose/ui/ui/src/androidInstrumentedTest/kotlin/androidx/compose/ui/platform/AndroidClipboardManagerTest.kt
+++ b/compose/ui/ui/src/androidInstrumentedTest/kotlin/androidx/compose/ui/platform/AndroidClipboardManagerTest.kt
@@ -19,6 +19,8 @@
 import android.content.ClipData
 import android.content.ClipDescription
 import android.content.ClipboardManager
+import android.net.Uri
+import androidx.compose.ui.ExperimentalComposeUiApi
 import androidx.compose.ui.geometry.Offset
 import androidx.compose.ui.graphics.Color
 import androidx.compose.ui.graphics.Shadow
@@ -38,6 +40,7 @@
 import com.google.common.truth.Truth.assertThat
 import org.junit.Test
 import org.junit.runner.RunWith
+import org.mockito.kotlin.doReturn
 import org.mockito.kotlin.mock
 import org.mockito.kotlin.never
 import org.mockito.kotlin.times
@@ -293,6 +296,44 @@
         verify(clipboardManager, times(1)).setPrimaryClip(clipData)
     }
 
+    @OptIn(ExperimentalComposeUiApi::class)
+    @Test
+    fun firstUriOrNull_returnsFirstItem_ifNotNull() {
+        val uri = Uri.parse("http://example.com")
+        val clipData = mock<ClipData> {
+            on { itemCount } doReturn 2
+            on { getItemAt(0) } doReturn ClipData.Item(uri)
+            on { getItemAt(1) } doReturn ClipData.Item("Hello")
+        }
+
+        assertThat(clipData.toClipEntry().firstUriOrNull()).isEqualTo(uri)
+    }
+
+    @OptIn(ExperimentalComposeUiApi::class)
+    @Test
+    fun firstUriOrNull_returnsSecondItem_ifFirstIsNull() {
+        val uri = Uri.parse("http://example.com")
+        val clipData = mock<ClipData> {
+            on { itemCount } doReturn 2
+            on { getItemAt(0) } doReturn ClipData.Item("Hello")
+            on { getItemAt(1) } doReturn ClipData.Item(uri)
+        }
+
+        assertThat(clipData.toClipEntry().firstUriOrNull()).isEqualTo(uri)
+    }
+
+    @OptIn(ExperimentalComposeUiApi::class)
+    @Test
+    fun firstUriOrNull_returnsNull_ifNoUri() {
+        val clipData = mock<ClipData> {
+            on { itemCount } doReturn 2
+            on { getItemAt(0) } doReturn ClipData.Item("Hello")
+            on { getItemAt(1) } doReturn ClipData.Item("World")
+        }
+
+        assertThat(clipData.toClipEntry().firstUriOrNull()).isNull()
+    }
+
     private fun assertEncodeAndDecode(spanStyle: SpanStyle) {
         val encoder = EncodeHelper()
         encoder.encode(spanStyle)
diff --git a/compose/ui/ui/src/androidInstrumentedTest/kotlin/androidx/compose/ui/platform/AndroidComposeViewScreenCoordinatesTest.kt b/compose/ui/ui/src/androidInstrumentedTest/kotlin/androidx/compose/ui/platform/AndroidComposeViewScreenCoordinatesTest.kt
index 28504fb..a48ccc0 100644
--- a/compose/ui/ui/src/androidInstrumentedTest/kotlin/androidx/compose/ui/platform/AndroidComposeViewScreenCoordinatesTest.kt
+++ b/compose/ui/ui/src/androidInstrumentedTest/kotlin/androidx/compose/ui/platform/AndroidComposeViewScreenCoordinatesTest.kt
@@ -45,6 +45,7 @@
 import androidx.savedstate.findViewTreeSavedStateRegistryOwner
 import androidx.savedstate.setViewTreeSavedStateRegistryOwner
 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 kotlin.test.assertNotNull
@@ -92,6 +93,7 @@
         }
     }
 
+    @SdkSuppress(maxSdkVersion = 33) // b/321823937
     @Test
     fun positionOnScreen_withNoComposableOffset() {
         rule.runOnIdle {
@@ -107,6 +109,7 @@
         }
     }
 
+    @SdkSuppress(maxSdkVersion = 33) // b/321823937
     @Test
     fun positionOnScreen_withComposableOffset() {
         rule.runOnIdle {
@@ -123,6 +126,7 @@
         }
     }
 
+    @SdkSuppress(maxSdkVersion = 33) // b/321823937
     @Test
     fun positionOnScreen_changesAfterUpdate() {
         rule.runOnIdle {
@@ -150,6 +154,7 @@
         }
     }
 
+    @SdkSuppress(maxSdkVersion = 33) // b/321823937
     @Test
     fun screenToLocal_withNoComposableOffset() {
         rule.runOnIdle {
@@ -165,6 +170,7 @@
         }
     }
 
+    @SdkSuppress(maxSdkVersion = 33) // b/321823937
     @Test
     fun screenToLocal_withComposableOffset() {
         rule.runOnIdle {
@@ -181,6 +187,7 @@
         }
     }
 
+    @SdkSuppress(maxSdkVersion = 33) // b/321823937
     @Test
     fun transformToScreen_fromIdentity_withNoComposableOffset() {
         val matrix = Matrix()
@@ -198,6 +205,7 @@
         }
     }
 
+    @SdkSuppress(maxSdkVersion = 33) // b/321823937
     @Test
     fun transformToScreen_fromIdentity_withComposableOffset() {
         val matrix = Matrix()
@@ -216,6 +224,7 @@
         }
     }
 
+    @SdkSuppress(maxSdkVersion = 33) // b/321823937
     @Test
     fun transformToScreen_changesAfterUpdate() {
         val matrix = Matrix()
@@ -245,6 +254,7 @@
         }
     }
 
+    @SdkSuppress(maxSdkVersion = 33) // b/321823937
     @Test
     fun transformToScreen_fromTransformedMatrix_includesExistingTransformation() {
         val matrix = Matrix()
diff --git a/compose/ui/ui/src/androidInstrumentedTest/kotlin/androidx/compose/ui/window/PopupTest.kt b/compose/ui/ui/src/androidInstrumentedTest/kotlin/androidx/compose/ui/window/PopupTest.kt
index df8fecb..c8cde80 100644
--- a/compose/ui/ui/src/androidInstrumentedTest/kotlin/androidx/compose/ui/window/PopupTest.kt
+++ b/compose/ui/ui/src/androidInstrumentedTest/kotlin/androidx/compose/ui/window/PopupTest.kt
@@ -31,7 +31,6 @@
 import androidx.compose.runtime.mutableStateOf
 import androidx.compose.runtime.setValue
 import androidx.compose.ui.Alignment
-import androidx.compose.ui.ExperimentalComposeUiApi
 import androidx.compose.ui.Modifier
 import androidx.compose.ui.layout.Layout
 import androidx.compose.ui.layout.onGloballyPositioned
@@ -363,7 +362,6 @@
         rule.onNodeWithTag(testTag).assertIsDisplayed()
     }
 
-    @OptIn(ExperimentalComposeUiApi::class)
     @Test
     fun canFillScreenWidth_dependingOnProperty() {
         var box1Width = 0
@@ -385,7 +383,6 @@
         }
     }
 
-    @OptIn(ExperimentalComposeUiApi::class)
     @Test
     fun canChangeSize() {
         var width by mutableStateOf(10.dp)
diff --git a/compose/ui/ui/src/androidMain/kotlin/androidx/compose/ui/contentcapture/AndroidContentCaptureManager.android.kt b/compose/ui/ui/src/androidMain/kotlin/androidx/compose/ui/contentcapture/AndroidContentCaptureManager.android.kt
index 6dc75ce..9ff7b8a 100644
--- a/compose/ui/ui/src/androidMain/kotlin/androidx/compose/ui/contentcapture/AndroidContentCaptureManager.android.kt
+++ b/compose/ui/ui/src/androidMain/kotlin/androidx/compose/ui/contentcapture/AndroidContentCaptureManager.android.kt
@@ -37,13 +37,11 @@
 import androidx.compose.ui.internal.checkPreconditionNotNull
 import androidx.compose.ui.node.LayoutNode
 import androidx.compose.ui.platform.AndroidComposeView
-import androidx.compose.ui.platform.ScrollObservationScope
 import androidx.compose.ui.platform.SemanticsNodeCopy
 import androidx.compose.ui.platform.SemanticsNodeWithAdjustedBounds
 import androidx.compose.ui.platform.coreshims.ContentCaptureSessionCompat
 import androidx.compose.ui.platform.coreshims.ViewCompatShims
 import androidx.compose.ui.platform.coreshims.ViewStructureCompat
-import androidx.compose.ui.platform.findById
 import androidx.compose.ui.platform.getAllUncoveredSemanticsNodesToIntObjectMap
 import androidx.compose.ui.platform.getTextLayoutResult
 import androidx.compose.ui.platform.toLegacyClassName
@@ -56,7 +54,6 @@
 import androidx.compose.ui.util.fastJoinToString
 import androidx.compose.ui.util.fastMap
 import androidx.core.util.keyIterator
-import androidx.core.view.accessibility.AccessibilityNodeProviderCompat
 import androidx.lifecycle.DefaultLifecycleObserver
 import androidx.lifecycle.LifecycleOwner
 import java.util.function.Consumer
@@ -292,11 +289,6 @@
         }
     }
 
-    private val scrollObservationScopes = mutableListOf<ScrollObservationScope>()
-    private val scheduleScrollEventIfNeededLambda: (ScrollObservationScope) -> Unit = {
-        this.scheduleScrollEventIfNeeded(it)
-    }
-
     // Analogous to `sendSemanticsPropertyChangeEvents`
     private fun checkForContentCapturePropertyChanges(
         newSemanticsNodes: IntObjectMap<SemanticsNodeWithAdjustedBounds>
@@ -331,81 +323,11 @@
                             sendContentCaptureTextUpdateEvent(newNode.id, newText.toString())
                         }
                     }
-                    SemanticsProperties.VerticalScrollAxisRange -> {
-                        notifySubtreeStateChangeIfNeeded(newNode.layoutNode)
-                        val scope = checkPreconditionNotNull(scrollObservationScopes.findById(id)) {
-                            "scroll observation scope does not exist"
-                        }
-                        scope.horizontalScrollAxisRange = newNode.unmergedConfig.getOrNull(
-                            SemanticsProperties.HorizontalScrollAxisRange
-                        )
-                        scope.verticalScrollAxisRange = newNode.unmergedConfig.getOrNull(
-                            SemanticsProperties.VerticalScrollAxisRange
-                        )
-                        scheduleScrollEventIfNeeded(scope)
-                    }
                 }
             }
         }
     }
 
-    // TODO(mnuzen): this code is copied over from a11y delegate, we want to centralize some
-    // of it into `SemanticsManager` to keep the code cleaner.
-    private fun scheduleScrollEventIfNeeded(scrollObservationScope: ScrollObservationScope) {
-        if (!scrollObservationScope.isValidOwnerScope) {
-            return
-        }
-        view.snapshotObserver.observeReads(
-            scrollObservationScope,
-            scheduleScrollEventIfNeededLambda
-        ) {
-            val newXState = scrollObservationScope.horizontalScrollAxisRange
-            val newYState = scrollObservationScope.verticalScrollAxisRange
-            val oldXValue = scrollObservationScope.oldXValue
-            val oldYValue = scrollObservationScope.oldYValue
-
-            val deltaX = if (newXState != null && oldXValue != null) {
-                newXState.value() - oldXValue
-            } else {
-                0f
-            }
-            val deltaY = if (newYState != null && oldYValue != null) {
-                newYState.value() - oldYValue
-            } else {
-                0f
-            }
-
-            if (deltaX != 0f || deltaY != 0f) {
-                val scrollerId = semanticsNodeIdToAccessibilityVirtualNodeId(
-                    scrollObservationScope.semanticsNodeId
-                )
-
-                view.invalidate()
-
-                currentSemanticsNodes[scrollerId]?.semanticsNode?.layoutNode?.let { layoutNode ->
-                    // Schedule a content subtree change event for the scroller. As side effects
-                    // this will also schedule a TYPE_VIEW_SCROLLED event, and suppress separate
-                    // events from being sent for each child whose bounds moved.
-                    notifySubtreeStateChangeIfNeeded(layoutNode)
-                }
-            }
-
-            if (newXState != null) {
-                scrollObservationScope.oldXValue = newXState.value()
-            }
-            if (newYState != null) {
-                scrollObservationScope.oldYValue = newYState.value()
-            }
-        }
-    }
-
-    private fun semanticsNodeIdToAccessibilityVirtualNodeId(id: Int): Int {
-        if (id == view.semanticsOwner.unmergedRootSemanticsNode.id) {
-            return AccessibilityNodeProviderCompat.HOST_VIEW_ID
-        }
-        return id
-    }
-
     private fun sendContentCaptureTextUpdateEvent(id: Int, newText: String) {
         if (Build.VERSION.SDK_INT < Build.VERSION_CODES.Q) {
             return
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 064defc..c8f31f0 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
@@ -21,7 +21,12 @@
 import android.content.Context
 import android.content.res.Configuration
 import android.graphics.Rect
-import android.os.Build
+import android.os.Build.VERSION.SDK_INT
+import android.os.Build.VERSION_CODES.M
+import android.os.Build.VERSION_CODES.N
+import android.os.Build.VERSION_CODES.O
+import android.os.Build.VERSION_CODES.Q
+import android.os.Build.VERSION_CODES.S
 import android.os.Looper
 import android.os.SystemClock
 import android.util.LongSparseArray
@@ -506,7 +511,7 @@
         context.resources.configuration.fontWeightAdjustmentCompat
 
     private val Configuration.fontWeightAdjustmentCompat: Int
-        get() = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) fontWeightAdjustment else 0
+        get() = if (SDK_INT >= S) fontWeightAdjustment else 0
 
     // Backed by mutableStateOf so that the ambient provider recomposes when it changes
     override var layoutDirection by mutableStateOf(
@@ -643,11 +648,8 @@
         }
     }
 
-    private val matrixToWindow = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
-        CalculateMatrixToWindowApi29()
-    } else {
-        CalculateMatrixToWindowApi21(tmpMatrix)
-    }
+    private val matrixToWindow =
+        if (SDK_INT < Q) CalculateMatrixToWindowApi21(tmpMatrix) else CalculateMatrixToWindowApi29()
 
     /**
      * Keyboard modifiers state might be changed when window is not focused, so window doesn't
@@ -664,7 +666,7 @@
         addOnAttachStateChangeListener(contentCaptureManager)
         setWillNotDraw(false)
         isFocusable = true
-        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
+        if (SDK_INT >= O) {
             AndroidComposeViewVerificationHelperMethodsO.focusable(
                 this,
                 focusable = View.FOCUSABLE,
@@ -677,10 +679,9 @@
         ViewRootForTest.onViewCreatedCallback?.invoke(this)
         setOnDragListener(this.dragAndDropModifierOnDragListener)
         root.attach(this)
-        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
-            // Support for this feature in Compose is tracked here: b/207654434
-            AndroidComposeViewForceDarkModeQ.disallowForceDark(this)
-        }
+
+        // Support for this feature in Compose is tracked here: b/207654434
+        if (SDK_INT >= Q) AndroidComposeViewForceDarkModeQ.disallowForceDark(this)
     }
 
     /**
@@ -892,18 +893,20 @@
             drawDragDecoration = drawDragDecoration,
         )
         @Suppress("DEPRECATION")
-        return if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N)
+        return if (SDK_INT >= N) {
             AndroidComposeViewStartDragAndDropN.startDragAndDrop(
                 view = this,
                 transferData = transferData,
                 dragShadowBuilder = shadowBuilder,
             )
-        else startDrag(
-            transferData.clipData,
-            shadowBuilder,
-            transferData.localState,
-            transferData.flags,
-        )
+        } else {
+            startDrag(
+                transferData.clipData,
+                shadowBuilder,
+                transferData.localState,
+                transferData.flags,
+            )
+        }
     }
 
     private fun clearChildInvalidObservations(viewGroup: ViewGroup) {
@@ -1255,10 +1258,7 @@
         // We can't be confident that RenderNode is supported, so we try and fail over to
         // the ViewLayer implementation. We'll try even on on P devices, but it will fail
         // until ART allows things on the unsupported list on P.
-        if (isHardwareAccelerated &&
-            Build.VERSION.SDK_INT >= Build.VERSION_CODES.M &&
-            isRenderNodeCompatible
-        ) {
+        if (isHardwareAccelerated && SDK_INT >= M && isRenderNodeCompatible) {
             try {
                 return RenderNodeLayer(
                     this,
@@ -1294,8 +1294,9 @@
         // wasn't easy to decode, so this work-around keeps up to 10 Views active
         // only for L. On other versions, it uses the WeakHashMap to retain as many
         // as are convenient.
-        val cacheValue = viewLayersContainer == null || ViewLayer.shouldUseDispatchDraw ||
-            Build.VERSION.SDK_INT >= Build.VERSION_CODES.M ||
+        val cacheValue = viewLayersContainer == null ||
+            ViewLayer.shouldUseDispatchDraw ||
+            SDK_INT >= M ||
             layerCache.size < MaximumLayerCacheSize
         if (cacheValue) {
             layerCache.push(layer)
@@ -1500,7 +1501,7 @@
         viewTreeObserver.addOnScrollChangedListener(scrollChangedListener)
         viewTreeObserver.addOnTouchModeChangeListener(touchModeChangeListener)
 
-        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) {
+        if (SDK_INT >= S) {
             AndroidComposeViewTranslationCallbackS.setViewTranslationCallback(
                 this,
                 AndroidComposeViewTranslationCallback()
@@ -1525,9 +1526,7 @@
         viewTreeObserver.removeOnScrollChangedListener(scrollChangedListener)
         viewTreeObserver.removeOnTouchModeChangeListener(touchModeChangeListener)
 
-        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) {
-            AndroidComposeViewTranslationCallbackS.clearViewTranslationCallback(this)
-        }
+        if (SDK_INT >= S) AndroidComposeViewTranslationCallbackS.clearViewTranslationCallback(this)
     }
 
     override fun onProvideAutofillVirtualStructure(structure: ViewStructure?, flags: Int) {
@@ -1538,7 +1537,7 @@
         if (autofillSupported()) _autofill?.performAutofill(values)
     }
 
-    @RequiresApi(Build.VERSION_CODES.S)
+    @RequiresApi(S)
     override fun onCreateVirtualViewTranslationRequests(
         virtualIds: LongArray,
         supportedFormats: IntArray,
@@ -1549,7 +1548,7 @@
         )
     }
 
-    @RequiresApi(Build.VERSION_CODES.S)
+    @RequiresApi(S)
     override fun onVirtualViewTranslationResponses(
         response: LongSparseArray<ViewTranslationResponse?>
     ) {
@@ -1930,7 +1929,7 @@
         }
     }
 
-    private fun autofillSupported() = Build.VERSION.SDK_INT >= Build.VERSION_CODES.O
+    private fun autofillSupported() = SDK_INT >= O
 
     public override fun dispatchHoverEvent(event: MotionEvent): Boolean {
         if (hoverExitReceived) {
@@ -1984,12 +1983,9 @@
         if (!eventInvalid) {
             // First event x,y is checked above if block, so we can skip index 0.
             for (index in 1 until event.pointerCount) {
-                eventInvalid = !event.getX(index).isFinite() || !event.getY(index).isFinite() ||
-                    if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
-                        !isValidMotionEvent(event, index)
-                    } else {
-                        false
-                    }
+                eventInvalid = !event.getX(index).isFinite() ||
+                    !event.getY(index).isFinite() ||
+                    (SDK_INT >= Q && !isValidMotionEvent(event, index))
 
                 if (eventInvalid) break
             }
@@ -2011,7 +2007,7 @@
         accessibilityId: Int,
         currentView: View
     ): View? {
-        if (Build.VERSION.SDK_INT < Build.VERSION_CODES.Q) {
+        if (SDK_INT < Q) {
             val getAccessibilityViewIdMethod = Class.forName("android.view.View")
                 .getDeclaredMethod("getAccessibilityViewId")
             getAccessibilityViewIdMethod.isAccessible = true
@@ -2043,7 +2039,7 @@
 
             override fun setIcon(value: PointerIcon?) {
                 currentIcon = value ?: PointerIcon.Default
-                if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) {
+                if (SDK_INT >= N) {
                     AndroidComposeViewVerificationHelperMethodsN.setPointerIcon(
                         this@AndroidComposeView,
                         currentIcon
@@ -2070,7 +2066,7 @@
             // invoke the hidden parent method after Android P. If in new android, the hidden method
             // ViewGroup#findViewByAccessibilityIdTraversal signature is changed or removed, we can
             // simply return null here because there will be no call to this method.
-            return if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
+            return if (SDK_INT >= Q) {
                 val findViewByAccessibilityIdTraversalMethod = Class.forName("android.view.View")
                     .getDeclaredMethod("findViewByAccessibilityIdTraversal", Int::class.java)
                 findViewByAccessibilityIdTraversalMethod.isAccessible = true
@@ -2125,7 +2121,7 @@
         val savedStateRegistryOwner: SavedStateRegistryOwner
     )
 
-    @RequiresApi(Build.VERSION_CODES.S)
+    @RequiresApi(S)
     private class AndroidComposeViewTranslationCallback : ViewTranslationCallback {
         override fun onShowTranslation(view: View): Boolean {
             val androidComposeView = view as AndroidComposeView
@@ -2152,9 +2148,9 @@
  * AOT compiled. It is expected that this class will soft-fail verification, but the classes
  * which use this method will pass.
  */
-@RequiresApi(Build.VERSION_CODES.O)
+@RequiresApi(O)
 private object AndroidComposeViewVerificationHelperMethodsO {
-    @RequiresApi(Build.VERSION_CODES.O)
+    @RequiresApi(O)
     @DoNotInline
     fun focusable(view: View, focusable: Int, defaultFocusHighlightEnabled: Boolean) {
         view.focusable = focusable
@@ -2163,10 +2159,10 @@
     }
 }
 
-@RequiresApi(Build.VERSION_CODES.N)
+@RequiresApi(N)
 private object AndroidComposeViewVerificationHelperMethodsN {
     @DoNotInline
-    @RequiresApi(Build.VERSION_CODES.N)
+    @RequiresApi(N)
     fun setPointerIcon(view: View, icon: PointerIcon?) {
         val iconToSet = when (icon) {
             is AndroidPointerIcon ->
@@ -2188,25 +2184,25 @@
     }
 }
 
-@RequiresApi(Build.VERSION_CODES.Q)
+@RequiresApi(Q)
 private object AndroidComposeViewForceDarkModeQ {
     @DoNotInline
-    @RequiresApi(Build.VERSION_CODES.Q)
+    @RequiresApi(Q)
     fun disallowForceDark(view: View) {
         view.isForceDarkAllowed = false
     }
 }
 
-@RequiresApi(Build.VERSION_CODES.S)
+@RequiresApi(S)
 internal object AndroidComposeViewTranslationCallbackS {
     @DoNotInline
-    @RequiresApi(Build.VERSION_CODES.S)
+    @RequiresApi(S)
     fun setViewTranslationCallback(view: View, translationCallback: ViewTranslationCallback) {
         view.setViewTranslationCallback(translationCallback)
     }
 
     @DoNotInline
-    @RequiresApi(Build.VERSION_CODES.S)
+    @RequiresApi(S)
     fun clearViewTranslationCallback(view: View) {
         view.clearViewTranslationCallback()
     }
@@ -2274,7 +2270,7 @@
     fun calculateMatrixToWindow(view: View, matrix: Matrix)
 }
 
-@RequiresApi(Build.VERSION_CODES.Q)
+@RequiresApi(Q)
 private class CalculateMatrixToWindowApi29 : CalculateMatrixToWindow {
     private val tmpMatrix = android.graphics.Matrix()
     private val tmpPosition = IntArray(2)
@@ -2351,10 +2347,10 @@
     }
 }
 
-@RequiresApi(Build.VERSION_CODES.N)
+@RequiresApi(N)
 private object AndroidComposeViewStartDragAndDropN {
     @DoNotInline
-    @RequiresApi(Build.VERSION_CODES.N)
+    @RequiresApi(N)
     fun startDragAndDrop(
         view: View,
         transferData: DragAndDropTransferData,
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 1ed409d..f3d1165 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
@@ -52,6 +52,7 @@
 import androidx.collection.mutableIntListOf
 import androidx.collection.mutableIntObjectMapOf
 import androidx.collection.mutableIntSetOf
+import androidx.collection.mutableLongObjectMapOf
 import androidx.collection.mutableObjectIntMapOf
 import androidx.compose.ui.ExperimentalComposeUiApi
 import androidx.compose.ui.R
@@ -95,6 +96,7 @@
 import androidx.compose.ui.util.fastForEachIndexed
 import androidx.compose.ui.util.fastJoinToString
 import androidx.compose.ui.util.fastRoundToInt
+import androidx.compose.ui.util.packInts
 import androidx.core.view.AccessibilityDelegateCompat
 import androidx.core.view.ViewCompat.ACCESSIBILITY_LIVE_REGION_ASSERTIVE
 import androidx.core.view.ViewCompat.ACCESSIBILITY_LIVE_REGION_POLITE
@@ -887,11 +889,13 @@
         }
         info.isClickable = false
         semanticsNode.unmergedConfig.getOrNull(SemanticsActions.OnClick)?.let {
-            // Selectable items that are already selected should not announce it again
+            // Selectable tabs and radio buttons that are already selected cannot be selected again
+            // so they should not be exposed as clickable.
             val isSelected =
                 semanticsNode.unmergedConfig.getOrNull(SemanticsProperties.Selected) == true
-            info.isClickable = !isSelected
-            if (semanticsNode.enabled() && !isSelected) {
+            val isRadioButtonOrTab = role == Role.Tab || role == Role.RadioButton
+            info.isClickable = !isRadioButtonOrTab || (isRadioButtonOrTab && !isSelected)
+            if (semanticsNode.enabled() && info.isClickable) {
                 info.addAction(
                     AccessibilityActionCompat(
                         AccessibilityNodeInfoCompat.ACTION_CLICK,
@@ -1375,38 +1379,56 @@
 
     // This needs to be here instead of around line 3000 because we need access to the `view`
     // that is inside the `AndroidComposeViewAccessibilityDelegateCompat` class
-    @OptIn(InternalTextApi::class)
     private fun getInfoText(
         node: SemanticsNode
+    ): AnnotatedString? {
+        val editableTextToAssign = node.unmergedConfig.getTextForTextField()
+        val textToAssign = node.unmergedConfig.getOrNull(SemanticsProperties.Text)?.firstOrNull()
+        return editableTextToAssign ?: textToAssign
+    }
+
+    @OptIn(InternalTextApi::class)
+    private fun AnnotatedString.toSpannableString(
+        node: SemanticsNode
     ): SpannableString? {
         val fontFamilyResolver: FontFamily.Resolver = view.fontFamilyResolver
-        val editableTextToAssign = trimToSize(
-            node.unmergedConfig.getTextForTextField()
-                ?.toAccessibilitySpannableString(
-                    density = view.density,
-                    fontFamilyResolver,
-                    urlSpanCache
-                ),
-            ParcelSafeTextLength
-        )
 
-        val textToAssign = trimToSize(
-            node.unmergedConfig.getOrNull(SemanticsProperties.Text)?.firstOrNull()
-                ?.toAccessibilitySpannableString(
-                    density = view.density,
-                    fontFamilyResolver,
-                    urlSpanCache
-                ),
+        val linkActions = if (hasLinkAnnotations(0, length)) {
+            // handle hyperlinks in text
+            val rangesToCustomActions = mutableLongObjectMapOf<() -> Unit>()
+            node.children.fastForEach { childNode ->
+                // hyperlink nodes pass onLinkClick actions through custom actions
+                val config = childNode.unmergedConfig
+                if (config.contains(SemanticsProperties.TextSelectionRange)) {
+                    val range = config[SemanticsProperties.TextSelectionRange]
+                    val action = config.getOrNull(CustomActions)?.firstOrNull()?.action
+                    action?.let {
+                        rangesToCustomActions[packInts(range.start, range.end)] = { it() }
+                    }
+                }
+            }
+            rangesToCustomActions
+        } else {
+            null
+        }
+
+        return trimToSize(
+            toAccessibilitySpannableString(
+                density = view.density,
+                fontFamilyResolver = fontFamilyResolver,
+                urlSpanCache = urlSpanCache,
+                linkActions = linkActions,
+                accessibilityNodeId = node.id
+            ),
             ParcelSafeTextLength
         )
-        return editableTextToAssign ?: textToAssign
     }
 
     private fun setText(
         node: SemanticsNode,
         info: AccessibilityNodeInfoCompat,
     ) {
-        info.text = getInfoText(node)
+        info.text = getInfoText(node)?.toSpannableString(node)
     }
 
     /**
diff --git a/compose/ui/ui/src/androidMain/kotlin/androidx/compose/ui/platform/ClipboardExtensions.android.kt b/compose/ui/ui/src/androidMain/kotlin/androidx/compose/ui/platform/ClipboardExtensions.android.kt
index 9223794..32ec68d 100644
--- a/compose/ui/ui/src/androidMain/kotlin/androidx/compose/ui/platform/ClipboardExtensions.android.kt
+++ b/compose/ui/ui/src/androidMain/kotlin/androidx/compose/ui/platform/ClipboardExtensions.android.kt
@@ -21,12 +21,17 @@
 import androidx.compose.ui.ExperimentalComposeUiApi
 
 /**
- * Returns the uri of the first item in this [ClipEntry].
+ * Returns the first non-null [Uri] from the list of [ClipData.Item]s in this [ClipEntry].
  *
- * Do not forget that each [ClipEntry] can contain multiple items in its [ClipData], therefore it
- * can have multiple Uris. Always check whether you are processing all the items in a given
- * [ClipEntry].
+ * Do not forget that each [ClipEntry] can contain multiple [ClipData.Item]s in its [ClipData],
+ * therefore it can have multiple [Uri]s. Always check whether you are processing all the items in
+ * a given [ClipEntry].
  */
 @ExperimentalComposeUiApi
-fun ClipEntry.firstUriOrNull(): Uri? =
-    clipData.takeIf { it.itemCount > 0 }?.getItemAt(0)?.uri
+fun ClipEntry.firstUriOrNull(): Uri? {
+    for (i in 0 until clipData.itemCount) {
+        val uri = clipData.getItemAt(i).uri
+        if (uri != null) return uri
+    }
+    return null
+}
diff --git a/compose/ui/ui/src/androidMain/kotlin/androidx/compose/ui/window/AndroidPopup.android.kt b/compose/ui/ui/src/androidMain/kotlin/androidx/compose/ui/window/AndroidPopup.android.kt
index 818c02a..c2879c2 100644
--- a/compose/ui/ui/src/androidMain/kotlin/androidx/compose/ui/window/AndroidPopup.android.kt
+++ b/compose/ui/ui/src/androidMain/kotlin/androidx/compose/ui/window/AndroidPopup.android.kt
@@ -54,7 +54,6 @@
 import androidx.compose.runtime.setValue
 import androidx.compose.runtime.snapshots.SnapshotStateObserver
 import androidx.compose.ui.Alignment
-import androidx.compose.ui.ExperimentalComposeUiApi
 import androidx.compose.ui.Modifier
 import androidx.compose.ui.R
 import androidx.compose.ui.UiComposable
@@ -113,7 +112,7 @@
  * the platform default, which is smaller than the screen width.
  */
 @Immutable
-actual class PopupProperties @ExperimentalComposeUiApi constructor(
+actual class PopupProperties constructor(
     actual val focusable: Boolean = false,
     actual val dismissOnBackPress: Boolean = true,
     actual val dismissOnClickOutside: Boolean = true,
@@ -136,7 +135,6 @@
         clippingEnabled = clippingEnabled,
     )
 
-    @OptIn(ExperimentalComposeUiApi::class)
     constructor(
         focusable: Boolean = false,
         dismissOnBackPress: Boolean = true,
diff --git a/compose/ui/ui/src/androidUnitTest/kotlin/androidx/compose/ui/node/DelegatingNodeTest.kt b/compose/ui/ui/src/androidUnitTest/kotlin/androidx/compose/ui/node/DelegatingNodeTest.kt
index 157de10..0f54238 100644
--- a/compose/ui/ui/src/androidUnitTest/kotlin/androidx/compose/ui/node/DelegatingNodeTest.kt
+++ b/compose/ui/ui/src/androidUnitTest/kotlin/androidx/compose/ui/node/DelegatingNodeTest.kt
@@ -79,6 +79,24 @@
     }
 
     @Test
+    fun testNestedDelegatesHaveNodePointersCorrectlyUpdated() {
+        val d = DrawMod()
+        val c = DrawMod()
+        val b = object : DelegatingNode() {
+            val c = delegate(c)
+        }
+        val a = object : DelegatingNode() {
+            val b = delegate(b)
+            val d = delegate(d)
+        }
+
+        assert(a.node === a)
+        assert(b.node === a)
+        assert(c.node === a)
+        assert(d.node === a)
+    }
+
+    @Test
     fun testAsKindReturnsNestedDelegate() {
         val node = DelegatedWrapper { DelegatedWrapper { DrawMod() } }
         assert(node.isKind(Nodes.Draw))
diff --git a/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/Modifier.kt b/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/Modifier.kt
index 4117eac3..bc482d1 100644
--- a/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/Modifier.kt
+++ b/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/Modifier.kt
@@ -369,7 +369,7 @@
             requireOwner().registerOnEndApplyChangesListener(effect)
         }
 
-        internal fun setAsDelegateTo(owner: Node) {
+        internal open fun setAsDelegateTo(owner: Node) {
             node = owner
         }
     }
diff --git a/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/internal/InlineClassHelper.kt b/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/internal/InlineClassHelper.kt
index b335162..693613b 100644
--- a/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/internal/InlineClassHelper.kt
+++ b/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/internal/InlineClassHelper.kt
@@ -22,11 +22,15 @@
 // This function exists so we do *not* inline the throw. It keeps
 // the call site much smaller and since it's the slow path anyway,
 // we don't mind the extra function call
-internal fun throwIllegalStateException(message: String): Nothing {
+internal fun throwIllegalStateException(message: String) {
     throw IllegalStateException(message)
 }
 
-internal fun throwIllegalArgumentException(message: String): Nothing {
+internal fun throwIllegalStateExceptionForNullCheck(message: String): Nothing {
+    throw IllegalStateException(message)
+}
+
+internal fun throwIllegalArgumentException(message: String) {
     throw IllegalArgumentException(message)
 }
 
@@ -64,7 +68,7 @@
     }
 
     if (value == null) {
-        throwIllegalStateException(lazyMessage())
+        throwIllegalStateExceptionForNullCheck(lazyMessage())
     }
 
     return value
@@ -79,7 +83,7 @@
     }
 
     if (value == null) {
-        throwIllegalStateException("Required value was null.")
+        throwIllegalStateExceptionForNullCheck("Required value was null.")
     }
 
     return value
diff --git a/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/node/DelegatableNode.kt b/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/node/DelegatableNode.kt
index 0bb8838..255fa31 100644
--- a/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/node/DelegatableNode.kt
+++ b/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/node/DelegatableNode.kt
@@ -35,8 +35,8 @@
 interface DelegatableNode {
     /**
      * A reference of the [Modifier.Node] that holds this node's position in the node hierarchy. If
-     * the node is a delegate of another node, this will point to that node. Otherwise, this will
-     * point to itself.
+     * the node is a delegate of another node, this will point to the root delegating node that is
+     * actually part of the node tree. Otherwise, this will point to itself.
      */
     val node: Modifier.Node
 }
diff --git a/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/node/DelegatingNode.kt b/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/node/DelegatingNode.kt
index 44963ed..099e391 100644
--- a/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/node/DelegatingNode.kt
+++ b/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/node/DelegatingNode.kt
@@ -56,6 +56,16 @@
     @TestOnly
     internal fun undelegateUnprotected(instance: DelegatableNode) = undelegate(instance)
 
+    override fun setAsDelegateTo(owner: Modifier.Node) {
+        super.setAsDelegateTo(owner)
+        // At this point _this_ node is being delegated to, however _this_ node may also
+        // have delegates of its own, and their current `node` pointers need to be updated
+        // so that they point to the right node in the tree.
+        forEachImmediateDelegate {
+            it.setAsDelegateTo(owner)
+        }
+    }
+
     /**
      * In order to properly delegate work to another [Modifier.Node], the delegated instance must
      * be created and returned inside of a [delegate] call. Doing this will
diff --git a/datastore/datastore-core/src/androidMain/cpp/CMakeLists.txt b/datastore/datastore-core/src/androidMain/cpp/CMakeLists.txt
index f7cba81..a6bd4f3 100644
--- a/datastore/datastore-core/src/androidMain/cpp/CMakeLists.txt
+++ b/datastore/datastore-core/src/androidMain/cpp/CMakeLists.txt
@@ -24,4 +24,9 @@
 add_library(shared_counter STATIC shared_counter.cc)
 add_library(datastore_shared_counter SHARED jni/androidx_datastore_core_SharedCounter.cc)
 
-target_link_libraries(datastore_shared_counter shared_counter)
\ No newline at end of file
+target_link_libraries(datastore_shared_counter shared_counter)
+target_link_options(
+    datastore_shared_counter
+    PRIVATE
+    "-Wl,-z,max-page-size=16384"
+)
diff --git a/development/project-creator/native-template/groupId/artifactId/src/main/cpp/CMakeLists.txt b/development/project-creator/native-template/groupId/artifactId/src/main/cpp/CMakeLists.txt
index 005698f7..3c1ddca 100644
--- a/development/project-creator/native-template/groupId/artifactId/src/main/cpp/CMakeLists.txt
+++ b/development/project-creator/native-template/groupId/artifactId/src/main/cpp/CMakeLists.txt
@@ -9,4 +9,6 @@
 set_property(TARGET <NAME>
         APPEND_STRING PROPERTY
         LINK_FLAGS
-        " -Wl,--version-script=${CMAKE_CURRENT_SOURCE_DIR}/version_scripts/libname.map.txt")
\ No newline at end of file
+        " -Wl,--version-script=${CMAKE_CURRENT_SOURCE_DIR}/version_scripts/libname.map.txt")
+
+target_link_options(<NAME> PRIVATE "-Wl,-z,max-page-size=16384")
diff --git a/docs-public/build.gradle b/docs-public/build.gradle
index 6abd071..7414d46 100644
--- a/docs-public/build.gradle
+++ b/docs-public/build.gradle
@@ -248,31 +248,31 @@
     docs("androidx.media2:media2-widget:1.3.0")
     docs("androidx.media:media:1.7.0")
     // androidx.media3 is not hosted in androidx
-    docsWithoutApiSince("androidx.media3:media3-cast:1.3.0-alpha01")
-    docsWithoutApiSince("androidx.media3:media3-common:1.3.0-alpha01")
-    docsWithoutApiSince("androidx.media3:media3-container:1.3.0-alpha01")
-    docsWithoutApiSince("androidx.media3:media3-database:1.3.0-alpha01")
-    docsWithoutApiSince("androidx.media3:media3-datasource:1.3.0-alpha01")
-    docsWithoutApiSince("androidx.media3:media3-datasource-cronet:1.3.0-alpha01")
-    docsWithoutApiSince("androidx.media3:media3-datasource-okhttp:1.3.0-alpha01")
-    docsWithoutApiSince("androidx.media3:media3-datasource-rtmp:1.3.0-alpha01")
-    docsWithoutApiSince("androidx.media3:media3-decoder:1.3.0-alpha01")
-    docsWithoutApiSince("androidx.media3:media3-effect:1.3.0-alpha01")
-    docsWithoutApiSince("androidx.media3:media3-exoplayer:1.3.0-alpha01")
-    docsWithoutApiSince("androidx.media3:media3-exoplayer-dash:1.3.0-alpha01")
-    docsWithoutApiSince("androidx.media3:media3-exoplayer-hls:1.3.0-alpha01")
-    docsWithoutApiSince("androidx.media3:media3-exoplayer-ima:1.3.0-alpha01")
-    docsWithoutApiSince("androidx.media3:media3-exoplayer-rtsp:1.3.0-alpha01")
-    docsWithoutApiSince("androidx.media3:media3-exoplayer-smoothstreaming:1.3.0-alpha01")
-    docsWithoutApiSince("androidx.media3:media3-exoplayer-workmanager:1.3.0-alpha01")
-    docsWithoutApiSince("androidx.media3:media3-extractor:1.3.0-alpha01")
-    docsWithoutApiSince("androidx.media3:media3-muxer:1.3.0-alpha01")
-    docsWithoutApiSince("androidx.media3:media3-session:1.3.0-alpha01")
-    docsWithoutApiSince("androidx.media3:media3-test-utils:1.3.0-alpha01")
-    docsWithoutApiSince("androidx.media3:media3-test-utils-robolectric:1.3.0-alpha01")
-    docsWithoutApiSince("androidx.media3:media3-transformer:1.3.0-alpha01")
-    docsWithoutApiSince("androidx.media3:media3-ui:1.3.0-alpha01")
-    docsWithoutApiSince("androidx.media3:media3-ui-leanback:1.3.0-alpha01")
+    docsWithoutApiSince("androidx.media3:media3-cast:1.3.0-beta01")
+    docsWithoutApiSince("androidx.media3:media3-common:1.3.0-beta01")
+    docsWithoutApiSince("androidx.media3:media3-container:1.3.0-beta01")
+    docsWithoutApiSince("androidx.media3:media3-database:1.3.0-beta01")
+    docsWithoutApiSince("androidx.media3:media3-datasource:1.3.0-beta01")
+    docsWithoutApiSince("androidx.media3:media3-datasource-cronet:1.3.0-beta01")
+    docsWithoutApiSince("androidx.media3:media3-datasource-okhttp:1.3.0-beta01")
+    docsWithoutApiSince("androidx.media3:media3-datasource-rtmp:1.3.0-beta01")
+    docsWithoutApiSince("androidx.media3:media3-decoder:1.3.0-beta01")
+    docsWithoutApiSince("androidx.media3:media3-effect:1.3.0-beta01")
+    docsWithoutApiSince("androidx.media3:media3-exoplayer:1.3.0-beta01")
+    docsWithoutApiSince("androidx.media3:media3-exoplayer-dash:1.3.0-beta01")
+    docsWithoutApiSince("androidx.media3:media3-exoplayer-hls:1.3.0-beta01")
+    docsWithoutApiSince("androidx.media3:media3-exoplayer-ima:1.3.0-beta01")
+    docsWithoutApiSince("androidx.media3:media3-exoplayer-rtsp:1.3.0-beta01")
+    docsWithoutApiSince("androidx.media3:media3-exoplayer-smoothstreaming:1.3.0-beta01")
+    docsWithoutApiSince("androidx.media3:media3-exoplayer-workmanager:1.3.0-beta01")
+    docsWithoutApiSince("androidx.media3:media3-extractor:1.3.0-beta01")
+    docsWithoutApiSince("androidx.media3:media3-muxer:1.3.0-beta01")
+    docsWithoutApiSince("androidx.media3:media3-session:1.3.0-beta01")
+    docsWithoutApiSince("androidx.media3:media3-test-utils:1.3.0-beta01")
+    docsWithoutApiSince("androidx.media3:media3-test-utils-robolectric:1.3.0-beta01")
+    docsWithoutApiSince("androidx.media3:media3-transformer:1.3.0-beta01")
+    docsWithoutApiSince("androidx.media3:media3-ui:1.3.0-beta01")
+    docsWithoutApiSince("androidx.media3:media3-ui-leanback:1.3.0-beta01")
     docs("androidx.mediarouter:mediarouter:1.7.0-alpha01")
     docs("androidx.mediarouter:mediarouter-testing:1.7.0-alpha01")
     docs("androidx.metrics:metrics-performance:1.0.0-beta01")
@@ -356,25 +356,25 @@
     docs("androidx.startup:startup-runtime:1.2.0-alpha02")
     docs("androidx.swiperefreshlayout:swiperefreshlayout:1.2.0-alpha01")
     // androidx.test is not hosted in androidx\
-    docsWithoutApiSince("androidx.test:core:1.6.0-alpha03")
-    docsWithoutApiSince("androidx.test:core-ktx:1.6.0-alpha03")
-    docsWithoutApiSince("androidx.test:monitor:1.7.0-alpha03")
-    docsWithoutApiSince("androidx.test:rules:1.6.0-alpha02")
-    docsWithoutApiSince("androidx.test:runner:1.6.0-alpha05")
-    docsWithoutApiSince("androidx.test.espresso:espresso-accessibility:3.6.0-alpha02")
-    docsWithoutApiSince("androidx.test.espresso:espresso-contrib:3.6.0-alpha02")
-    docsWithoutApiSince("androidx.test.espresso:espresso-core:3.6.0-alpha02")
-    docsWithoutApiSince("androidx.test.espresso:espresso-device:1.0.0-alpha07")
-    docsWithoutApiSince("androidx.test.espresso:espresso-idling-resource:3.6.0-alpha02")
-    docsWithoutApiSince("androidx.test.espresso:espresso-intents:3.6.0-alpha02")
-    docsWithoutApiSince("androidx.test.espresso:espresso-remote:3.6.0-alpha02")
-    docsWithoutApiSince("androidx.test.espresso:espresso-web:3.6.0-alpha02")
-    docsWithoutApiSince("androidx.test.espresso.idling:idling-concurrent:3.6.0-alpha02")
-    docsWithoutApiSince("androidx.test.espresso.idling:idling-net:3.6.0-alpha02")
-    docsWithoutApiSince("androidx.test.ext:junit:1.2.0-alpha02")
-    docsWithoutApiSince("androidx.test.ext:junit-ktx:1.2.0-alpha02")
-    docsWithoutApiSince("androidx.test.ext:truth:1.6.0-alpha02")
-    docsWithoutApiSince("androidx.test.services:storage:1.5.0-alpha02")
+    docsWithoutApiSince("androidx.test:core:1.6.0-alpha05")
+    docsWithoutApiSince("androidx.test:core-ktx:1.6.0-alpha05")
+    docsWithoutApiSince("androidx.test:monitor:1.7.0-alpha04")
+    docsWithoutApiSince("androidx.test:rules:1.6.0-alpha03")
+    docsWithoutApiSince("androidx.test:runner:1.6.0-alpha06")
+    docsWithoutApiSince("androidx.test.espresso:espresso-accessibility:3.6.0-alpha03")
+    docsWithoutApiSince("androidx.test.espresso:espresso-contrib:3.6.0-alpha03")
+    docsWithoutApiSince("androidx.test.espresso:espresso-core:3.6.0-alpha03")
+    docsWithoutApiSince("androidx.test.espresso:espresso-device:1.0.0-alpha08")
+    docsWithoutApiSince("androidx.test.espresso:espresso-idling-resource:3.6.0-alpha03")
+    docsWithoutApiSince("androidx.test.espresso:espresso-intents:3.6.0-alpha03")
+    docsWithoutApiSince("androidx.test.espresso:espresso-remote:3.6.0-alpha03")
+    docsWithoutApiSince("androidx.test.espresso:espresso-web:3.6.0-alpha03")
+    docsWithoutApiSince("androidx.test.espresso.idling:idling-concurrent:3.6.0-alpha03")
+    docsWithoutApiSince("androidx.test.espresso.idling:idling-net:3.6.0-alpha03")
+    docsWithoutApiSince("androidx.test.ext:junit:1.2.0-alpha03")
+    docsWithoutApiSince("androidx.test.ext:junit-ktx:1.2.0-alpha03")
+    docsWithoutApiSince("androidx.test.ext:truth:1.6.0-alpha03")
+    docsWithoutApiSince("androidx.test.services:storage:1.5.0-alpha03")
     docsWithoutApiSince("androidx.test.uiautomator:uiautomator:2.3.0-beta01")
     // androidx.textclassifier is not hosted in androidx
     docsWithoutApiSince("androidx.textclassifier:textclassifier:1.0.0-alpha04")
diff --git a/docs-tip-of-tree/build.gradle b/docs-tip-of-tree/build.gradle
index c7d7e44..7357f62 100644
--- a/docs-tip-of-tree/build.gradle
+++ b/docs-tip-of-tree/build.gradle
@@ -22,7 +22,6 @@
     // ads-identifier is deprecated
     kmpDocs(project(":annotation:annotation"))
     docs(project(":annotation:annotation-experimental"))
-    docs(project(":annotation:annotation-replacewith"))
     docs(project(":appactions:builtintypes:builtintypes"))
     samples(project(":appactions:builtintypes:builtintypes:builtintypes-samples"))
     docs(project(":appactions:interaction:interaction-capabilities-communication"))
diff --git a/emoji2/emoji2-emojipicker/src/main/java/androidx/emoji2/emojipicker/EmojiPickerPopupBidirectionalDesign.kt b/emoji2/emoji2-emojipicker/src/main/java/androidx/emoji2/emojipicker/EmojiPickerPopupBidirectionalDesign.kt
new file mode 100644
index 0000000..6e78f50
--- /dev/null
+++ b/emoji2/emoji2-emojipicker/src/main/java/androidx/emoji2/emojipicker/EmojiPickerPopupBidirectionalDesign.kt
@@ -0,0 +1,103 @@
+/*
+ * Copyright 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.emoji2.emojipicker
+
+import android.content.Context
+import android.view.Gravity
+import android.view.View
+import android.view.ViewGroup
+import android.widget.FrameLayout
+import android.widget.LinearLayout
+import androidx.appcompat.widget.AppCompatImageView
+
+/**
+ * Emoji picker popup view with bidirectional UI design to switch emoji to face left or right.
+ */
+internal class EmojiPickerPopupBidirectionalDesign(
+    override val context: Context,
+    override val targetEmojiView: View,
+    override val variants: List<String>,
+    override val popupView: LinearLayout,
+    override val emojiViewOnClickListener: View.OnClickListener
+) : EmojiPickerPopupDesign() {
+    private var emojiFacingLeft = true
+
+    init {
+        updateTemplate()
+    }
+    override fun addLayoutHeader() {
+        val row = LinearLayout(context).apply {
+            orientation = LinearLayout.HORIZONTAL
+            gravity = Gravity.CENTER
+            layoutParams = LinearLayout.LayoutParams(
+                FrameLayout.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.WRAP_CONTENT)
+        }
+        FrameLayout.inflate(context, R.layout.emoji_picker_popup_bidirectional, row)
+            .findViewById<AppCompatImageView>(R.id.emoji_picker_popup_bidirectional_icon)
+            .apply {
+                layoutParams = LinearLayout.LayoutParams(
+                    targetEmojiView.width, targetEmojiView.height)
+            }
+        popupView.addView(row)
+        val imageView =
+            row.findViewById<AppCompatImageView>(R.id.emoji_picker_popup_bidirectional_icon)
+        imageView.setOnClickListener {
+            emojiFacingLeft = !emojiFacingLeft
+            updateTemplate()
+            popupView.removeViews( /* start= */1, getActualNumberOfRows())
+            addRowsToPopupView()
+        }
+    }
+
+    override fun getNumberOfRows(): Int {
+        // Adding one row for the bidirectional switcher.
+        return variants.size / 2 / BIDIRECTIONAL_COLUMN_COUNT + 1
+    }
+    override fun getNumberOfColumns(): Int {
+        return BIDIRECTIONAL_COLUMN_COUNT
+    }
+
+    private fun getActualNumberOfRows(): Int {
+        // Removing one extra row of the bidirectional switcher.
+        return getNumberOfRows() - 1
+    }
+
+    private fun updateTemplate() {
+        template = if (emojiFacingLeft)
+            arrayOf((variants.indices.filter { it % 12 < 6 }.map { it + 1 }).toIntArray())
+        else
+            arrayOf((variants.indices.filter { it % 12 >= 6 }.map { it + 1 }).toIntArray())
+
+        val row = getActualNumberOfRows()
+        val column = getNumberOfColumns()
+        val overrideTemplate = Array(row) { IntArray(column) }
+        var index = 0
+        for (i in 0 until row) {
+            for (j in 0 until column) {
+                if (index < template[0].size) {
+                    overrideTemplate[i][j] = template[0][index]
+                    index++
+                }
+            }
+        }
+        template = overrideTemplate
+    }
+
+    companion object {
+        private const val BIDIRECTIONAL_COLUMN_COUNT = 6
+    }
+}
diff --git a/emoji2/emoji2-emojipicker/src/main/java/androidx/emoji2/emojipicker/EmojiPickerPopupDesign.kt b/emoji2/emoji2-emojipicker/src/main/java/androidx/emoji2/emojipicker/EmojiPickerPopupDesign.kt
new file mode 100644
index 0000000..4688bf3
--- /dev/null
+++ b/emoji2/emoji2-emojipicker/src/main/java/androidx/emoji2/emojipicker/EmojiPickerPopupDesign.kt
@@ -0,0 +1,79 @@
+/*
+ * Copyright 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.emoji2.emojipicker
+
+import android.content.Context
+import android.view.View
+import android.view.ViewGroup
+import android.view.accessibility.AccessibilityEvent
+import android.widget.FrameLayout
+import android.widget.LinearLayout
+
+/**
+ * Emoji picker popup view UI design. Each UI design needs to inherit this abstract class.
+ */
+internal abstract class EmojiPickerPopupDesign {
+    abstract val context: Context
+    abstract val targetEmojiView: View
+    abstract val variants: List<String>
+    abstract val popupView: LinearLayout
+    abstract val emojiViewOnClickListener: View.OnClickListener
+    lateinit var template: Array<IntArray>
+    open fun addLayoutHeader() {
+        // no-ops
+    }
+    open fun addRowsToPopupView() {
+        for (row in template) {
+            val rowLayout = LinearLayout(context).apply {
+                orientation = LinearLayout.HORIZONTAL
+                layoutParams = LinearLayout.LayoutParams(
+                    FrameLayout.LayoutParams.WRAP_CONTENT, FrameLayout.LayoutParams.WRAP_CONTENT
+                )
+            }
+            for (item in row) {
+                val cell =
+                    if (item == 0) {
+                        EmojiView(context)
+                    } else {
+                        EmojiView(context).apply {
+                            willDrawVariantIndicator = false
+                            emoji = variants[item - 1]
+                            setOnClickListener(emojiViewOnClickListener)
+                            if (item == 1) {
+                                // Hover on the first emoji in the popup
+                                popupView.post {
+                                    sendAccessibilityEvent(AccessibilityEvent.TYPE_VIEW_HOVER_ENTER)
+                                }
+                            }
+                        }
+                    }.apply {
+                        layoutParams = ViewGroup.LayoutParams(
+                            targetEmojiView.width, targetEmojiView.height)
+                    }
+                rowLayout.addView(cell)
+            }
+            popupView.addView(rowLayout)
+        }
+    }
+
+    open fun addLayoutFooter() {
+        // no-ops
+    }
+
+    abstract fun getNumberOfRows(): Int
+    abstract fun getNumberOfColumns(): Int
+}
diff --git a/emoji2/emoji2-emojipicker/src/main/java/androidx/emoji2/emojipicker/EmojiPickerPopupFlatDesign.kt b/emoji2/emoji2-emojipicker/src/main/java/androidx/emoji2/emojipicker/EmojiPickerPopupFlatDesign.kt
new file mode 100644
index 0000000..916a473
--- /dev/null
+++ b/emoji2/emoji2-emojipicker/src/main/java/androidx/emoji2/emojipicker/EmojiPickerPopupFlatDesign.kt
@@ -0,0 +1,60 @@
+/*
+ * Copyright 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.emoji2.emojipicker
+
+import android.content.Context
+import android.view.View
+import android.widget.LinearLayout
+
+/**
+ * Emoji picker popup view with flat design to list emojis.
+ */
+internal class EmojiPickerPopupFlatDesign(
+    override val context: Context,
+    override val targetEmojiView: View,
+    override val variants: List<String>,
+    override val popupView: LinearLayout,
+    override val emojiViewOnClickListener: View.OnClickListener
+) : EmojiPickerPopupDesign() {
+    init {
+        template = arrayOf(variants.indices.map { it + 1 }.toIntArray())
+        var row = getNumberOfRows()
+        var column = getNumberOfColumns()
+        val overrideTemplate = Array(row) { IntArray(column) }
+        var index = 0
+        for (i in 0 until row) {
+            for (j in 0 until column) {
+                if (index < template[0].size) {
+                    overrideTemplate[i][j] = template[0][index]
+                    index++
+                }
+            }
+        }
+        template = overrideTemplate
+    }
+    override fun getNumberOfRows(): Int {
+        val column = getNumberOfColumns()
+        return variants.size / column + if (variants.size % column == 0) 0 else 1
+    }
+    override fun getNumberOfColumns(): Int {
+        return minOf(FLAT_COLUMN_MAX_COUNT, template[0].size)
+    }
+
+    companion object {
+        private const val FLAT_COLUMN_MAX_COUNT = 6
+    }
+}
diff --git a/emoji2/emoji2-emojipicker/src/main/java/androidx/emoji2/emojipicker/EmojiPickerPopupSquareDesign.kt b/emoji2/emoji2-emojipicker/src/main/java/androidx/emoji2/emojipicker/EmojiPickerPopupSquareDesign.kt
new file mode 100644
index 0000000..84f4595
--- /dev/null
+++ b/emoji2/emoji2-emojipicker/src/main/java/androidx/emoji2/emojipicker/EmojiPickerPopupSquareDesign.kt
@@ -0,0 +1,57 @@
+/*
+ * Copyright 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.emoji2.emojipicker
+
+import android.content.Context
+import android.view.View
+import android.widget.LinearLayout
+
+/**
+ * Emoji picker popup view with square design.
+ */
+internal class EmojiPickerPopupSquareDesign(
+    override val context: Context,
+    override val targetEmojiView: View,
+    override val variants: List<String>,
+    override val popupView: LinearLayout,
+    override val emojiViewOnClickListener: View.OnClickListener
+) : EmojiPickerPopupDesign() {
+    init {
+        template = SQUARE_LAYOUT_TEMPLATE
+    }
+    override fun getNumberOfRows(): Int {
+        return SQUARE_LAYOUT_TEMPLATE.size
+    }
+    override fun getNumberOfColumns(): Int {
+        return SQUARE_LAYOUT_TEMPLATE[0].size
+    }
+
+    companion object {
+        /**
+         * Square variant layout template without skin tone.
+         * 0 : a place holder
+         * Positive number is the index + 1 in the variant array
+         */
+        private val SQUARE_LAYOUT_TEMPLATE = arrayOf(
+            intArrayOf(0, 2, 3, 4, 5, 6),
+            intArrayOf(0, 7, 8, 9, 10, 11),
+            intArrayOf(0, 12, 13, 14, 15, 16),
+            intArrayOf(0, 17, 18, 19, 20, 21),
+            intArrayOf(1, 22, 23, 24, 25, 26)
+        )
+    }
+}
diff --git a/emoji2/emoji2-emojipicker/src/main/java/androidx/emoji2/emojipicker/EmojiPickerPopupSquareWithSkintoneCircleDesign.kt b/emoji2/emoji2-emojipicker/src/main/java/androidx/emoji2/emojipicker/EmojiPickerPopupSquareWithSkintoneCircleDesign.kt
new file mode 100644
index 0000000..3bac29a
--- /dev/null
+++ b/emoji2/emoji2-emojipicker/src/main/java/androidx/emoji2/emojipicker/EmojiPickerPopupSquareWithSkintoneCircleDesign.kt
@@ -0,0 +1,133 @@
+/*
+ * Copyright 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.emoji2.emojipicker
+
+import android.content.Context
+import android.graphics.Canvas
+import android.graphics.Paint
+import android.util.AttributeSet
+import android.view.View
+import android.view.ViewGroup
+import android.view.accessibility.AccessibilityEvent
+import android.widget.FrameLayout
+import android.widget.LinearLayout
+import androidx.core.content.ContextCompat
+
+/**
+ * Emoji picker popup view with square design which has skin tone circles to indicate the
+ * combination of the skin tones.
+ */
+internal class EmojiPickerPopupSquareWithSkintoneCircleDesign(
+    override val context: Context,
+    override val targetEmojiView: View,
+    override val variants: List<String>,
+    override val popupView: LinearLayout,
+    override val emojiViewOnClickListener: View.OnClickListener
+) : EmojiPickerPopupDesign() {
+    override fun addRowsToPopupView() {
+        for (row in SQUARE_LAYOUT_WITH_SKIN_TONES_TEMPLATE) {
+            val rowLayout = LinearLayout(context).apply {
+                orientation = LinearLayout.HORIZONTAL
+                layoutParams = LinearLayout.LayoutParams(
+                    FrameLayout.LayoutParams.WRAP_CONTENT, FrameLayout.LayoutParams.WRAP_CONTENT
+                )
+            }
+            for (item in row) {
+                val cell = when (item) {
+                    in 1..variants.size ->
+                        EmojiView(context).apply {
+                            willDrawVariantIndicator = false
+                            emoji = variants[item - 1]
+                            setOnClickListener(emojiViewOnClickListener)
+                            if (item == 1) {
+                                // Hover on the first emoji in the popup
+                                popupView.post {
+                                    sendAccessibilityEvent(AccessibilityEvent.TYPE_VIEW_HOVER_ENTER)
+                                }
+                            }
+                        }
+
+                    0 -> EmojiView(context)
+
+                    else -> SkinToneCircleView(context).apply {
+                        paint = Paint().apply {
+                            color = ContextCompat.getColor(
+                                context, SKIN_TONE_COLOR_RES_IDS[item + 5])
+                            style = Paint.Style.FILL
+                        }
+                    }
+                }.apply {
+                    layoutParams = ViewGroup.LayoutParams(
+                        targetEmojiView.width, targetEmojiView.height)
+                }
+                rowLayout.addView(cell)
+            }
+            popupView.addView(rowLayout)
+        }
+    }
+
+    override fun getNumberOfRows(): Int {
+        return SQUARE_LAYOUT_WITH_SKIN_TONES_TEMPLATE.size
+    }
+    override fun getNumberOfColumns(): Int {
+        return SQUARE_LAYOUT_WITH_SKIN_TONES_TEMPLATE[0].size
+    }
+
+    companion object {
+        private val SKIN_TONE_COLOR_RES_IDS = listOf(
+            R.color.light_skin_tone,
+            R.color.medium_light_skin_tone,
+            R.color.medium_skin_tone,
+            R.color.medium_dark_skin_tone,
+            R.color.dark_skin_tone
+        )
+
+        /**
+         * Square variant layout template with skin tone.
+         * 0 : a place holder
+         * -5: light skin tone circle
+         * -4: medium-light skin tone circle
+         * -3: medium skin tone circle
+         * -2: medium-dark skin tone circle
+         * -1: dark skin tone circle
+         * Positive number is the index + 1 in the variant array
+         */
+        private val SQUARE_LAYOUT_WITH_SKIN_TONES_TEMPLATE = arrayOf(
+            intArrayOf(0, 0, -5, -4, -3, -2, -1),
+            intArrayOf(0, -5, 2, 3, 4, 5, 6),
+            intArrayOf(0, -4, 7, 8, 9, 10, 11),
+            intArrayOf(0, -3, 12, 13, 14, 15, 16),
+            intArrayOf(0, -2, 17, 18, 19, 20, 21),
+            intArrayOf(1, -1, 22, 23, 24, 25, 26)
+        )
+    }
+}
+
+internal class SkinToneCircleView @JvmOverloads constructor(
+    context: Context,
+    attrs: AttributeSet? = null
+) : View(context, attrs) {
+    private val radius = resources.getDimension(R.dimen.emoji_picker_skin_tone_circle_radius)
+    var paint: Paint? = null
+
+    override fun draw(canvas: Canvas) {
+        super.draw(canvas)
+        canvas.apply {
+            paint?.let { drawCircle(width / 2f, height / 2f, radius, it) }
+        }
+    }
+}
diff --git a/emoji2/emoji2-emojipicker/src/main/java/androidx/emoji2/emojipicker/EmojiPickerPopupView.kt b/emoji2/emoji2-emojipicker/src/main/java/androidx/emoji2/emojipicker/EmojiPickerPopupView.kt
index 3c1f944..4921a38 100644
--- a/emoji2/emoji2-emojipicker/src/main/java/androidx/emoji2/emojipicker/EmojiPickerPopupView.kt
+++ b/emoji2/emoji2-emojipicker/src/main/java/androidx/emoji2/emojipicker/EmojiPickerPopupView.kt
@@ -17,17 +17,10 @@
 package androidx.emoji2.emojipicker
 
 import android.content.Context
-import android.graphics.Canvas
-import android.graphics.Paint
 import android.util.AttributeSet
-import android.view.Gravity
 import android.view.View
-import android.view.ViewGroup
-import android.view.accessibility.AccessibilityEvent
 import android.widget.FrameLayout
 import android.widget.LinearLayout
-import androidx.appcompat.widget.AppCompatImageView
-import androidx.core.content.ContextCompat
 
 /** Popup view for emoji picker to show emoji variants. */
 internal class EmojiPickerPopupView @JvmOverloads constructor(
@@ -40,150 +33,47 @@
 ) :
     FrameLayout(context, attrs, defStyleAttr) {
     private val popupView: LinearLayout
-    private var layoutTemplate: LayoutTemplate
-    private var emojiFacingLeft = true
-
+    private val popupDesign: EmojiPickerPopupDesign
     init {
         popupView = inflate(context, R.layout.variant_popup, /* root= */ null)
             .findViewById<LinearLayout>(R.id.variant_popup)
-        layoutTemplate = getLayoutTemplate(variants)
-        if (layoutTemplate.layout == Layout.BIDIRECTIONAL) {
-            addBidirectionalLayoutHeader(popupView)
+        val layout = getLayout()
+        popupDesign = when (layout) {
+            Layout.FLAT -> EmojiPickerPopupFlatDesign(
+                context, targetEmojiView, variants, popupView, emojiViewOnClickListener)
+            Layout.SQUARE -> EmojiPickerPopupSquareDesign(
+                context, targetEmojiView, variants, popupView, emojiViewOnClickListener)
+            Layout.SQUARE_WITH_SKIN_TONE_CIRCLE -> EmojiPickerPopupSquareWithSkintoneCircleDesign(
+                context, targetEmojiView, variants, popupView, emojiViewOnClickListener)
+            Layout.BIDIRECTIONAL -> EmojiPickerPopupBidirectionalDesign(
+                context, targetEmojiView, variants, popupView, emojiViewOnClickListener)
         }
-        addRowsToPopupView()
+        popupDesign.addLayoutHeader()
+        popupDesign.addRowsToPopupView()
+        popupDesign.addLayoutFooter()
         addView(popupView)
     }
 
-    private fun addRowsToPopupView() {
-        for (row in layoutTemplate.template) {
-            val rowLayout = LinearLayout(context).apply {
-                orientation = LinearLayout.HORIZONTAL
-                layoutParams = LinearLayout.LayoutParams(
-                    LayoutParams.WRAP_CONTENT, LayoutParams.WRAP_CONTENT
-                )
-            }
-            for (item in row) {
-                val cell = when (item) {
-                    in 1..variants.size ->
-                        EmojiView(context).apply {
-                            willDrawVariantIndicator = false
-                            emoji = variants[item - 1]
-                            setOnClickListener(emojiViewOnClickListener)
-                            if (item == 1) {
-                                // Hover on the first emoji in the popup
-                                popupView.post {
-                                    sendAccessibilityEvent(AccessibilityEvent.TYPE_VIEW_HOVER_ENTER)
-                                }
-                            }
-                        }
-
-                    0 -> EmojiView(context)
-
-                    else -> SkinToneCircleView(context).apply {
-                        paint = Paint().apply {
-                            color = ContextCompat.getColor(
-                                context, SKIN_TONE_COLOR_RES_IDS[item + 5])
-                            style = Paint.Style.FILL
-                        }
-                    }
-                }.apply {
-                    layoutParams = ViewGroup.LayoutParams(
-                        targetEmojiView.width, targetEmojiView.height)
-                }
-                rowLayout.addView(cell)
-            }
-            popupView.addView(rowLayout)
-        }
-    }
-
     fun getPopupViewWidth(): Int {
-        return layoutTemplate.numberOfColumns * targetEmojiView.width +
+        return popupDesign.getNumberOfColumns() * targetEmojiView.width +
             popupView.paddingStart + popupView.paddingEnd
     }
 
     fun getPopupViewHeight(): Int {
-        val numberOfRows = if (layoutTemplate.layout == Layout.BIDIRECTIONAL)
-            layoutTemplate.numberOfRows + 1 else layoutTemplate.numberOfRows
-        return numberOfRows * targetEmojiView.height +
+        return popupDesign.getNumberOfRows() * targetEmojiView.height +
             popupView.paddingTop + popupView.paddingBottom
     }
 
-    private fun getLayoutTemplate(variants: List<String>): LayoutTemplate {
-        val layout =
-            if (variants.size == SQUARE_LAYOUT_VARIANT_COUNT)
-                if (SQUARE_LAYOUT_EMOJI_NO_SKIN_TONE.contains(variants[0]))
-                    Layout.SQUARE
-                else Layout.SQUARE_WITH_SKIN_TONE_CIRCLE
-            else if (variants.size == BIDIRECTIONAL_VARIANTS_COUNT)
-                Layout.BIDIRECTIONAL
+    private fun getLayout(): Layout {
+        if (variants.size == SQUARE_LAYOUT_VARIANT_COUNT)
+            if (SQUARE_LAYOUT_EMOJI_NO_SKIN_TONE.contains(variants[0]))
+                return Layout.SQUARE
             else
-                Layout.FLAT
-        var template = when (layout) {
-            Layout.SQUARE -> SQUARE_LAYOUT_TEMPLATE
-            Layout.SQUARE_WITH_SKIN_TONE_CIRCLE -> SQUARE_LAYOUT_WITH_SKIN_TONES_TEMPLATE
-            Layout.FLAT -> arrayOf(variants.indices.map { it + 1 }.toIntArray())
-            Layout.BIDIRECTIONAL ->
-                if (emojiFacingLeft)
-                    arrayOf((variants.indices.filter { it % 12 < 6 }.map { it + 1 }).toIntArray())
-                else
-                    arrayOf((variants.indices.filter { it % 12 >= 6 }.map { it + 1 }).toIntArray())
-        }
-        val column = when (layout) {
-            Layout.SQUARE, Layout.SQUARE_WITH_SKIN_TONE_CIRCLE -> template[0].size
-            Layout.FLAT, Layout.BIDIRECTIONAL -> minOf(6, template[0].size)
-        }
-        val row = when (layout) {
-            Layout.SQUARE, Layout.SQUARE_WITH_SKIN_TONE_CIRCLE -> template.size
-            Layout.FLAT -> variants.size / column + if (variants.size % column == 0) 0 else 1
-            Layout.BIDIRECTIONAL -> variants.size / 2 / column
-        }
-
-        // Rewrite template when the number of row mismatch
-        if (row != template.size) {
-            val overrideTemplate = Array(row) { IntArray(column) }
-            var index = 0
-            for (i in 0 until row) {
-                for (j in 0 until column) {
-                    if (index < template[0].size) {
-                        overrideTemplate[i][j] = template[0][index]
-                        index++
-                    }
-                }
-            }
-            template = overrideTemplate
-        }
-        return LayoutTemplate(layout, template, row, column)
-    }
-
-    private data class LayoutTemplate(
-        var layout: Layout,
-        val template: Array<IntArray>,
-        val numberOfRows: Int,
-        val numberOfColumns: Int
-    )
-
-    private fun addBidirectionalLayoutHeader(popupView: LinearLayout) {
-        val row = LinearLayout(context).apply {
-            orientation = LinearLayout.HORIZONTAL
-            gravity = Gravity.CENTER
-            layoutParams = LinearLayout.LayoutParams(
-                LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.WRAP_CONTENT)
-        }
-        inflate(context, R.layout.emoji_picker_popup_bidirectional, row)
-            .findViewById<AppCompatImageView>(R.id.emoji_picker_popup_bidirectional_icon)
-            .apply {
-                layoutParams = LinearLayout.LayoutParams(
-                    targetEmojiView.width, targetEmojiView.height)
-            }
-        popupView.addView(row)
-        val imageView =
-            row.findViewById<AppCompatImageView>(R.id.emoji_picker_popup_bidirectional_icon)
-        imageView.setOnClickListener {
-            emojiFacingLeft = !emojiFacingLeft
-            layoutTemplate = getLayoutTemplate(variants)
-            popupView.removeViews( /* start= */1, layoutTemplate.numberOfRows)
-            addRowsToPopupView()
-        }
+                return Layout.SQUARE_WITH_SKIN_TONE_CIRCLE
+        else if (variants.size == BIDIRECTIONAL_VARIANTS_COUNT)
+            return Layout.BIDIRECTIONAL
+        else
+            return Layout.FLAT
     }
 
     companion object {
@@ -205,60 +95,5 @@
 
         // Set of emojis that use the square layout without skin tone swatches.
         private val SQUARE_LAYOUT_EMOJI_NO_SKIN_TONE = setOf("👪")
-
-        private val SKIN_TONE_COLOR_RES_IDS = listOf(
-            R.color.light_skin_tone,
-            R.color.medium_light_skin_tone,
-            R.color.medium_skin_tone,
-            R.color.medium_dark_skin_tone,
-            R.color.dark_skin_tone
-        )
-
-        /**
-         * Square variant layout template with skin tone.
-         * 0 : a place holder
-         * -5: light skin tone circle
-         * -4: medium-light skin tone circle
-         * -3: medium skin tone circle
-         * -2: medium-dark skin tone circle
-         * -1: dark skin tone circle
-         * Positive number is the index + 1 in the variant array
-         */
-        private val SQUARE_LAYOUT_WITH_SKIN_TONES_TEMPLATE = arrayOf(
-            intArrayOf(0, 0, -5, -4, -3, -2, -1),
-            intArrayOf(0, -5, 2, 3, 4, 5, 6),
-            intArrayOf(0, -4, 7, 8, 9, 10, 11),
-            intArrayOf(0, -3, 12, 13, 14, 15, 16),
-            intArrayOf(0, -2, 17, 18, 19, 20, 21),
-            intArrayOf(1, -1, 22, 23, 24, 25, 26)
-        )
-
-        /**
-         * Square variant layout template without skin tone.
-         * 0 : a place holder
-         * Positive number is the index + 1 in the variant array
-         */
-        private val SQUARE_LAYOUT_TEMPLATE = arrayOf(
-            intArrayOf(0, 2, 3, 4, 5, 6),
-            intArrayOf(0, 7, 8, 9, 10, 11),
-            intArrayOf(0, 12, 13, 14, 15, 16),
-            intArrayOf(0, 17, 18, 19, 20, 21),
-            intArrayOf(1, 22, 23, 24, 25, 26)
-        )
-    }
-}
-
-internal class SkinToneCircleView @JvmOverloads constructor(
-    context: Context,
-    attrs: AttributeSet? = null
-) : View(context, attrs) {
-    private val radius = resources.getDimension(R.dimen.emoji_picker_skin_tone_circle_radius)
-    var paint: Paint? = null
-
-    override fun draw(canvas: Canvas) {
-        super.draw(canvas)
-        canvas.apply {
-            paint?.let { drawCircle(width / 2f, height / 2f, radius, it) }
-        }
     }
 }
diff --git a/emoji2/emoji2-emojipicker/src/main/res/values-af/strings.xml b/emoji2/emoji2-emojipicker/src/main/res/values-af/strings.xml
index ab3be47..8216c83 100644
--- a/emoji2/emoji2-emojipicker/src/main/res/values-af/strings.xml
+++ b/emoji2/emoji2-emojipicker/src/main/res/values-af/strings.xml
@@ -29,4 +29,6 @@
     <string name="emoji_category_flags" msgid="6185639503532784871">"VLAE"</string>
     <string name="emoji_empty_non_recent_category" msgid="288822832574892625">"Geen emosiekone beskikbaar nie"</string>
     <string name="emoji_empty_recent_category" msgid="7863877827879290200">"Jy het nog geen emosiekone gebruik nie"</string>
+    <!-- no translation found for emoji_bidirectional_switcher_content_desc (5084600168354220605) -->
+    <skip />
 </resources>
diff --git a/emoji2/emoji2-emojipicker/src/main/res/values-am/strings.xml b/emoji2/emoji2-emojipicker/src/main/res/values-am/strings.xml
index 57a237b..f42beb0 100644
--- a/emoji2/emoji2-emojipicker/src/main/res/values-am/strings.xml
+++ b/emoji2/emoji2-emojipicker/src/main/res/values-am/strings.xml
@@ -29,4 +29,6 @@
     <string name="emoji_category_flags" msgid="6185639503532784871">"ባንዲራዎች"</string>
     <string name="emoji_empty_non_recent_category" msgid="288822832574892625">"ምንም ስሜት ገላጭ ምስሎች አይገኙም"</string>
     <string name="emoji_empty_recent_category" msgid="7863877827879290200">"ምንም ስሜት ገላጭ ምስሎችን እስካሁን አልተጠቀሙም"</string>
+    <!-- no translation found for emoji_bidirectional_switcher_content_desc (5084600168354220605) -->
+    <skip />
 </resources>
diff --git a/emoji2/emoji2-emojipicker/src/main/res/values-ar/strings.xml b/emoji2/emoji2-emojipicker/src/main/res/values-ar/strings.xml
index f286364..3567ded 100644
--- a/emoji2/emoji2-emojipicker/src/main/res/values-ar/strings.xml
+++ b/emoji2/emoji2-emojipicker/src/main/res/values-ar/strings.xml
@@ -29,4 +29,6 @@
     <string name="emoji_category_flags" msgid="6185639503532784871">"الأعلام"</string>
     <string name="emoji_empty_non_recent_category" msgid="288822832574892625">"لا تتوفر أي رموز تعبيرية."</string>
     <string name="emoji_empty_recent_category" msgid="7863877827879290200">"لم تستخدم أي رموز تعبيرية حتى الآن."</string>
+    <!-- no translation found for emoji_bidirectional_switcher_content_desc (5084600168354220605) -->
+    <skip />
 </resources>
diff --git a/emoji2/emoji2-emojipicker/src/main/res/values-as/strings.xml b/emoji2/emoji2-emojipicker/src/main/res/values-as/strings.xml
index 60ef2a9..dea449f 100644
--- a/emoji2/emoji2-emojipicker/src/main/res/values-as/strings.xml
+++ b/emoji2/emoji2-emojipicker/src/main/res/values-as/strings.xml
@@ -29,4 +29,6 @@
     <string name="emoji_category_flags" msgid="6185639503532784871">"পতাকা"</string>
     <string name="emoji_empty_non_recent_category" msgid="288822832574892625">"কোনো ইম’জি উপলব্ধ নহয়"</string>
     <string name="emoji_empty_recent_category" msgid="7863877827879290200">"আপুনি এতিয়ালৈকে কোনো ইম’জি ব্যৱহাৰ কৰা নাই"</string>
+    <!-- no translation found for emoji_bidirectional_switcher_content_desc (5084600168354220605) -->
+    <skip />
 </resources>
diff --git a/emoji2/emoji2-emojipicker/src/main/res/values-az/strings.xml b/emoji2/emoji2-emojipicker/src/main/res/values-az/strings.xml
index 2668f55..41d1392 100644
--- a/emoji2/emoji2-emojipicker/src/main/res/values-az/strings.xml
+++ b/emoji2/emoji2-emojipicker/src/main/res/values-az/strings.xml
@@ -29,4 +29,6 @@
     <string name="emoji_category_flags" msgid="6185639503532784871">"BAYRAQLAR"</string>
     <string name="emoji_empty_non_recent_category" msgid="288822832574892625">"Əlçatan emoji yoxdur"</string>
     <string name="emoji_empty_recent_category" msgid="7863877827879290200">"Hələ heç bir emojidən istifadə etməməsiniz"</string>
+    <!-- no translation found for emoji_bidirectional_switcher_content_desc (5084600168354220605) -->
+    <skip />
 </resources>
diff --git a/emoji2/emoji2-emojipicker/src/main/res/values-b+sr+Latn/strings.xml b/emoji2/emoji2-emojipicker/src/main/res/values-b+sr+Latn/strings.xml
index baf2211..6b1da7f 100644
--- a/emoji2/emoji2-emojipicker/src/main/res/values-b+sr+Latn/strings.xml
+++ b/emoji2/emoji2-emojipicker/src/main/res/values-b+sr+Latn/strings.xml
@@ -29,4 +29,6 @@
     <string name="emoji_category_flags" msgid="6185639503532784871">"ZASTAVE"</string>
     <string name="emoji_empty_non_recent_category" msgid="288822832574892625">"Emodžiji nisu dostupni"</string>
     <string name="emoji_empty_recent_category" msgid="7863877827879290200">"Još niste koristili emodžije"</string>
+    <!-- no translation found for emoji_bidirectional_switcher_content_desc (5084600168354220605) -->
+    <skip />
 </resources>
diff --git a/emoji2/emoji2-emojipicker/src/main/res/values-be/strings.xml b/emoji2/emoji2-emojipicker/src/main/res/values-be/strings.xml
index 29f27f4..771b4d0e 100644
--- a/emoji2/emoji2-emojipicker/src/main/res/values-be/strings.xml
+++ b/emoji2/emoji2-emojipicker/src/main/res/values-be/strings.xml
@@ -29,4 +29,6 @@
     <string name="emoji_category_flags" msgid="6185639503532784871">"СЦЯГІ"</string>
     <string name="emoji_empty_non_recent_category" msgid="288822832574892625">"Няма даступных эмодзі"</string>
     <string name="emoji_empty_recent_category" msgid="7863877827879290200">"Вы пакуль не выкарыстоўвалі эмодзі"</string>
+    <!-- no translation found for emoji_bidirectional_switcher_content_desc (5084600168354220605) -->
+    <skip />
 </resources>
diff --git a/emoji2/emoji2-emojipicker/src/main/res/values-bg/strings.xml b/emoji2/emoji2-emojipicker/src/main/res/values-bg/strings.xml
index 0bc423e..9dd5152 100644
--- a/emoji2/emoji2-emojipicker/src/main/res/values-bg/strings.xml
+++ b/emoji2/emoji2-emojipicker/src/main/res/values-bg/strings.xml
@@ -29,4 +29,6 @@
     <string name="emoji_category_flags" msgid="6185639503532784871">"ЗНАМЕНА"</string>
     <string name="emoji_empty_non_recent_category" msgid="288822832574892625">"Няма налични емоджи"</string>
     <string name="emoji_empty_recent_category" msgid="7863877827879290200">"Все още не сте използвали емоджита"</string>
+    <!-- no translation found for emoji_bidirectional_switcher_content_desc (5084600168354220605) -->
+    <skip />
 </resources>
diff --git a/emoji2/emoji2-emojipicker/src/main/res/values-bn/strings.xml b/emoji2/emoji2-emojipicker/src/main/res/values-bn/strings.xml
index 02e9c03..115e0c4 100644
--- a/emoji2/emoji2-emojipicker/src/main/res/values-bn/strings.xml
+++ b/emoji2/emoji2-emojipicker/src/main/res/values-bn/strings.xml
@@ -29,4 +29,6 @@
     <string name="emoji_category_flags" msgid="6185639503532784871">"ফ্ল্যাগ"</string>
     <string name="emoji_empty_non_recent_category" msgid="288822832574892625">"কোনও ইমোজি উপলভ্য নেই"</string>
     <string name="emoji_empty_recent_category" msgid="7863877827879290200">"আপনি এখনও কোনও ইমোজি ব্যবহার করেননি"</string>
+    <!-- no translation found for emoji_bidirectional_switcher_content_desc (5084600168354220605) -->
+    <skip />
 </resources>
diff --git a/emoji2/emoji2-emojipicker/src/main/res/values-bs/strings.xml b/emoji2/emoji2-emojipicker/src/main/res/values-bs/strings.xml
index 29770bc..73d1e87 100644
--- a/emoji2/emoji2-emojipicker/src/main/res/values-bs/strings.xml
+++ b/emoji2/emoji2-emojipicker/src/main/res/values-bs/strings.xml
@@ -29,4 +29,6 @@
     <string name="emoji_category_flags" msgid="6185639503532784871">"ZASTAVE"</string>
     <string name="emoji_empty_non_recent_category" msgid="288822832574892625">"Emoji sličice nisu dostupne"</string>
     <string name="emoji_empty_recent_category" msgid="7863877827879290200">"Još niste koristili nijednu emoji sličicu"</string>
+    <!-- no translation found for emoji_bidirectional_switcher_content_desc (5084600168354220605) -->
+    <skip />
 </resources>
diff --git a/emoji2/emoji2-emojipicker/src/main/res/values-ca/strings.xml b/emoji2/emoji2-emojipicker/src/main/res/values-ca/strings.xml
index 251972d..7e1d139 100644
--- a/emoji2/emoji2-emojipicker/src/main/res/values-ca/strings.xml
+++ b/emoji2/emoji2-emojipicker/src/main/res/values-ca/strings.xml
@@ -29,4 +29,6 @@
     <string name="emoji_category_flags" msgid="6185639503532784871">"BANDERES"</string>
     <string name="emoji_empty_non_recent_category" msgid="288822832574892625">"No hi ha cap emoji disponible"</string>
     <string name="emoji_empty_recent_category" msgid="7863877827879290200">"Encara no has fet servir cap emoji"</string>
+    <!-- no translation found for emoji_bidirectional_switcher_content_desc (5084600168354220605) -->
+    <skip />
 </resources>
diff --git a/emoji2/emoji2-emojipicker/src/main/res/values-cs/strings.xml b/emoji2/emoji2-emojipicker/src/main/res/values-cs/strings.xml
index d1c8ad9..dc70333 100644
--- a/emoji2/emoji2-emojipicker/src/main/res/values-cs/strings.xml
+++ b/emoji2/emoji2-emojipicker/src/main/res/values-cs/strings.xml
@@ -29,4 +29,6 @@
     <string name="emoji_category_flags" msgid="6185639503532784871">"VLAJKY"</string>
     <string name="emoji_empty_non_recent_category" msgid="288822832574892625">"Nejsou k dispozici žádné smajlíky"</string>
     <string name="emoji_empty_recent_category" msgid="7863877827879290200">"Zatím jste žádná emodži nepoužili"</string>
+    <!-- no translation found for emoji_bidirectional_switcher_content_desc (5084600168354220605) -->
+    <skip />
 </resources>
diff --git a/emoji2/emoji2-emojipicker/src/main/res/values-da/strings.xml b/emoji2/emoji2-emojipicker/src/main/res/values-da/strings.xml
index 428d994..b09dbe4 100644
--- a/emoji2/emoji2-emojipicker/src/main/res/values-da/strings.xml
+++ b/emoji2/emoji2-emojipicker/src/main/res/values-da/strings.xml
@@ -29,4 +29,6 @@
     <string name="emoji_category_flags" msgid="6185639503532784871">"FLAG"</string>
     <string name="emoji_empty_non_recent_category" msgid="288822832574892625">"Der er ingen tilgængelige emojis"</string>
     <string name="emoji_empty_recent_category" msgid="7863877827879290200">"Du har ikke brugt nogen emojis endnu"</string>
+    <!-- no translation found for emoji_bidirectional_switcher_content_desc (5084600168354220605) -->
+    <skip />
 </resources>
diff --git a/emoji2/emoji2-emojipicker/src/main/res/values-de/strings.xml b/emoji2/emoji2-emojipicker/src/main/res/values-de/strings.xml
index 3bb43cb..9071a38 100644
--- a/emoji2/emoji2-emojipicker/src/main/res/values-de/strings.xml
+++ b/emoji2/emoji2-emojipicker/src/main/res/values-de/strings.xml
@@ -29,4 +29,6 @@
     <string name="emoji_category_flags" msgid="6185639503532784871">"FLAGGEN"</string>
     <string name="emoji_empty_non_recent_category" msgid="288822832574892625">"Keine Emojis verfügbar"</string>
     <string name="emoji_empty_recent_category" msgid="7863877827879290200">"Du hast noch keine Emojis verwendet"</string>
+    <!-- no translation found for emoji_bidirectional_switcher_content_desc (5084600168354220605) -->
+    <skip />
 </resources>
diff --git a/emoji2/emoji2-emojipicker/src/main/res/values-el/strings.xml b/emoji2/emoji2-emojipicker/src/main/res/values-el/strings.xml
index 79d4060..e12a7b7 100644
--- a/emoji2/emoji2-emojipicker/src/main/res/values-el/strings.xml
+++ b/emoji2/emoji2-emojipicker/src/main/res/values-el/strings.xml
@@ -29,4 +29,6 @@
     <string name="emoji_category_flags" msgid="6185639503532784871">"ΣΗΜΑΙΕΣ"</string>
     <string name="emoji_empty_non_recent_category" msgid="288822832574892625">"Δεν υπάρχουν διαθέσιμα emoji"</string>
     <string name="emoji_empty_recent_category" msgid="7863877827879290200">"Δεν έχετε χρησιμοποιήσει κανένα emoji ακόμα"</string>
+    <!-- no translation found for emoji_bidirectional_switcher_content_desc (5084600168354220605) -->
+    <skip />
 </resources>
diff --git a/emoji2/emoji2-emojipicker/src/main/res/values-en-rAU/strings.xml b/emoji2/emoji2-emojipicker/src/main/res/values-en-rAU/strings.xml
index 53fc79b4..244c602 100644
--- a/emoji2/emoji2-emojipicker/src/main/res/values-en-rAU/strings.xml
+++ b/emoji2/emoji2-emojipicker/src/main/res/values-en-rAU/strings.xml
@@ -29,4 +29,6 @@
     <string name="emoji_category_flags" msgid="6185639503532784871">"FLAGS"</string>
     <string name="emoji_empty_non_recent_category" msgid="288822832574892625">"No emojis available"</string>
     <string name="emoji_empty_recent_category" msgid="7863877827879290200">"You haven\'t used any emoji yet"</string>
+    <!-- no translation found for emoji_bidirectional_switcher_content_desc (5084600168354220605) -->
+    <skip />
 </resources>
diff --git a/emoji2/emoji2-emojipicker/src/main/res/values-en-rCA/strings.xml b/emoji2/emoji2-emojipicker/src/main/res/values-en-rCA/strings.xml
index ad9f1c2..7e406c9 100644
--- a/emoji2/emoji2-emojipicker/src/main/res/values-en-rCA/strings.xml
+++ b/emoji2/emoji2-emojipicker/src/main/res/values-en-rCA/strings.xml
@@ -29,4 +29,5 @@
     <string name="emoji_category_flags" msgid="6185639503532784871">"FLAGS"</string>
     <string name="emoji_empty_non_recent_category" msgid="288822832574892625">"No emojis available"</string>
     <string name="emoji_empty_recent_category" msgid="7863877827879290200">"You haven\'t used any emojis yet"</string>
+    <string name="emoji_bidirectional_switcher_content_desc" msgid="5084600168354220605">"emoji bidirectional switcher"</string>
 </resources>
diff --git a/emoji2/emoji2-emojipicker/src/main/res/values-en-rGB/strings.xml b/emoji2/emoji2-emojipicker/src/main/res/values-en-rGB/strings.xml
index 53fc79b4..244c602 100644
--- a/emoji2/emoji2-emojipicker/src/main/res/values-en-rGB/strings.xml
+++ b/emoji2/emoji2-emojipicker/src/main/res/values-en-rGB/strings.xml
@@ -29,4 +29,6 @@
     <string name="emoji_category_flags" msgid="6185639503532784871">"FLAGS"</string>
     <string name="emoji_empty_non_recent_category" msgid="288822832574892625">"No emojis available"</string>
     <string name="emoji_empty_recent_category" msgid="7863877827879290200">"You haven\'t used any emoji yet"</string>
+    <!-- no translation found for emoji_bidirectional_switcher_content_desc (5084600168354220605) -->
+    <skip />
 </resources>
diff --git a/emoji2/emoji2-emojipicker/src/main/res/values-en-rIN/strings.xml b/emoji2/emoji2-emojipicker/src/main/res/values-en-rIN/strings.xml
index 53fc79b4..244c602 100644
--- a/emoji2/emoji2-emojipicker/src/main/res/values-en-rIN/strings.xml
+++ b/emoji2/emoji2-emojipicker/src/main/res/values-en-rIN/strings.xml
@@ -29,4 +29,6 @@
     <string name="emoji_category_flags" msgid="6185639503532784871">"FLAGS"</string>
     <string name="emoji_empty_non_recent_category" msgid="288822832574892625">"No emojis available"</string>
     <string name="emoji_empty_recent_category" msgid="7863877827879290200">"You haven\'t used any emoji yet"</string>
+    <!-- no translation found for emoji_bidirectional_switcher_content_desc (5084600168354220605) -->
+    <skip />
 </resources>
diff --git a/emoji2/emoji2-emojipicker/src/main/res/values-en-rXC/strings.xml b/emoji2/emoji2-emojipicker/src/main/res/values-en-rXC/strings.xml
index 976e527..c1af752 100644
--- a/emoji2/emoji2-emojipicker/src/main/res/values-en-rXC/strings.xml
+++ b/emoji2/emoji2-emojipicker/src/main/res/values-en-rXC/strings.xml
@@ -29,4 +29,5 @@
     <string name="emoji_category_flags" msgid="6185639503532784871">"‎‏‎‎‎‎‎‏‎‏‏‏‎‎‎‎‎‎‏‎‎‏‎‎‎‎‏‏‏‏‏‏‏‏‎‏‎‏‎‏‏‏‎‏‎‏‏‏‏‏‎‎‏‏‏‎‎‏‎‏‏‎‏‏‎‎‎‎‎‏‎‎‏‎‏‏‎‎‎‏‏‏‏‏‏‏‎‎‏‏‏‎‎‏‏‏‎FLAGS‎‏‎‎‏‎"</string>
     <string name="emoji_empty_non_recent_category" msgid="288822832574892625">"‎‏‎‎‎‎‎‏‎‏‏‏‎‎‎‎‎‎‏‎‎‏‎‎‎‎‏‏‏‏‎‏‏‏‎‎‎‎‎‎‎‎‏‎‎‎‎‏‏‎‏‎‏‏‎‏‎‏‏‎‎‎‎‎‎‎‏‎‏‎‏‎‎‏‏‎‎‎‎‎‏‎‏‎‎‏‎‏‎‎‎‏‎No emojis available‎‏‎‎‏‎"</string>
     <string name="emoji_empty_recent_category" msgid="7863877827879290200">"‎‏‎‎‎‎‎‏‎‏‏‏‎‎‎‎‎‎‏‎‎‏‎‎‎‎‏‏‏‏‏‏‏‏‏‎‏‏‎‏‎‎‏‎‎‎‏‎‎‎‎‏‏‎‏‏‎‎‏‏‏‎‎‎‎‏‎‎‎‏‏‏‏‏‎‏‎‎‎‎‏‏‎‎‎‏‎‏‎‏‎‏‏‎‎‎‎You haven\'t used any emojis yet‎‏‎‎‏‎"</string>
+    <string name="emoji_bidirectional_switcher_content_desc" msgid="5084600168354220605">"‎‏‎‎‎‎‎‏‎‏‏‏‎‎‎‎‎‎‏‎‎‏‎‎‎‎‏‏‏‏‏‏‏‏‎‎‎‏‏‎‏‎‎‏‎‎‎‎‎‎‏‎‎‎‎‎‏‏‏‎‏‎‎‏‏‏‏‎‏‎‏‏‏‎‏‎‎‏‎‏‎‎‎‎‏‎‏‎‎‎‏‏‏‏‎‏‎emoji bidirectional switcher‎‏‎‎‏‎"</string>
 </resources>
diff --git a/emoji2/emoji2-emojipicker/src/main/res/values-es-rUS/strings.xml b/emoji2/emoji2-emojipicker/src/main/res/values-es-rUS/strings.xml
index 1d745ad..3879270 100644
--- a/emoji2/emoji2-emojipicker/src/main/res/values-es-rUS/strings.xml
+++ b/emoji2/emoji2-emojipicker/src/main/res/values-es-rUS/strings.xml
@@ -29,4 +29,6 @@
     <string name="emoji_category_flags" msgid="6185639503532784871">"BANDERAS"</string>
     <string name="emoji_empty_non_recent_category" msgid="288822832574892625">"No hay ningún emoji disponible"</string>
     <string name="emoji_empty_recent_category" msgid="7863877827879290200">"Todavía no usaste ningún emoji"</string>
+    <!-- no translation found for emoji_bidirectional_switcher_content_desc (5084600168354220605) -->
+    <skip />
 </resources>
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 bb95933..f438f76 100644
--- a/emoji2/emoji2-emojipicker/src/main/res/values-es/strings.xml
+++ b/emoji2/emoji2-emojipicker/src/main/res/values-es/strings.xml
@@ -29,4 +29,6 @@
     <string name="emoji_category_flags" msgid="6185639503532784871">"BANDERAS"</string>
     <string name="emoji_empty_non_recent_category" msgid="288822832574892625">"No hay emojis disponibles"</string>
     <string name="emoji_empty_recent_category" msgid="7863877827879290200">"Aún no has usado ningún emoji"</string>
+    <!-- no translation found for emoji_bidirectional_switcher_content_desc (5084600168354220605) -->
+    <skip />
 </resources>
diff --git a/emoji2/emoji2-emojipicker/src/main/res/values-et/strings.xml b/emoji2/emoji2-emojipicker/src/main/res/values-et/strings.xml
index 4ef5ed4..da92264 100644
--- a/emoji2/emoji2-emojipicker/src/main/res/values-et/strings.xml
+++ b/emoji2/emoji2-emojipicker/src/main/res/values-et/strings.xml
@@ -29,4 +29,6 @@
     <string name="emoji_category_flags" msgid="6185639503532784871">"LIPUD"</string>
     <string name="emoji_empty_non_recent_category" msgid="288822832574892625">"Ühtegi emotikoni pole saadaval"</string>
     <string name="emoji_empty_recent_category" msgid="7863877827879290200">"Te pole veel ühtegi emotikoni kasutanud"</string>
+    <!-- no translation found for emoji_bidirectional_switcher_content_desc (5084600168354220605) -->
+    <skip />
 </resources>
diff --git a/emoji2/emoji2-emojipicker/src/main/res/values-eu/strings.xml b/emoji2/emoji2-emojipicker/src/main/res/values-eu/strings.xml
index b72b3e6..d54498e 100644
--- a/emoji2/emoji2-emojipicker/src/main/res/values-eu/strings.xml
+++ b/emoji2/emoji2-emojipicker/src/main/res/values-eu/strings.xml
@@ -29,4 +29,6 @@
     <string name="emoji_category_flags" msgid="6185639503532784871">"BANDERAK"</string>
     <string name="emoji_empty_non_recent_category" msgid="288822832574892625">"Ez dago emotikonorik erabilgarri"</string>
     <string name="emoji_empty_recent_category" msgid="7863877827879290200">"Ez duzu erabili emojirik oraingoz"</string>
+    <!-- no translation found for emoji_bidirectional_switcher_content_desc (5084600168354220605) -->
+    <skip />
 </resources>
diff --git a/emoji2/emoji2-emojipicker/src/main/res/values-fa/strings.xml b/emoji2/emoji2-emojipicker/src/main/res/values-fa/strings.xml
index fe48f4d..4d5c29f 100644
--- a/emoji2/emoji2-emojipicker/src/main/res/values-fa/strings.xml
+++ b/emoji2/emoji2-emojipicker/src/main/res/values-fa/strings.xml
@@ -29,4 +29,6 @@
     <string name="emoji_category_flags" msgid="6185639503532784871">"پرچم‌ها"</string>
     <string name="emoji_empty_non_recent_category" msgid="288822832574892625">"اموجی دردسترس نیست"</string>
     <string name="emoji_empty_recent_category" msgid="7863877827879290200">"هنوز از هیچ اموجی‌ای استفاده نکرده‌اید"</string>
+    <!-- no translation found for emoji_bidirectional_switcher_content_desc (5084600168354220605) -->
+    <skip />
 </resources>
diff --git a/emoji2/emoji2-emojipicker/src/main/res/values-fi/strings.xml b/emoji2/emoji2-emojipicker/src/main/res/values-fi/strings.xml
index f1c636e..c0c08c4 100644
--- a/emoji2/emoji2-emojipicker/src/main/res/values-fi/strings.xml
+++ b/emoji2/emoji2-emojipicker/src/main/res/values-fi/strings.xml
@@ -29,4 +29,6 @@
     <string name="emoji_category_flags" msgid="6185639503532784871">"LIPUT"</string>
     <string name="emoji_empty_non_recent_category" msgid="288822832574892625">"Ei emojeita saatavilla"</string>
     <string name="emoji_empty_recent_category" msgid="7863877827879290200">"Et ole vielä käyttänyt emojeita"</string>
+    <!-- no translation found for emoji_bidirectional_switcher_content_desc (5084600168354220605) -->
+    <skip />
 </resources>
diff --git a/emoji2/emoji2-emojipicker/src/main/res/values-fr-rCA/strings.xml b/emoji2/emoji2-emojipicker/src/main/res/values-fr-rCA/strings.xml
index 1b3be2b..13e9ac7 100644
--- a/emoji2/emoji2-emojipicker/src/main/res/values-fr-rCA/strings.xml
+++ b/emoji2/emoji2-emojipicker/src/main/res/values-fr-rCA/strings.xml
@@ -29,4 +29,6 @@
     <string name="emoji_category_flags" msgid="6185639503532784871">"DRAPEAUX"</string>
     <string name="emoji_empty_non_recent_category" msgid="288822832574892625">"Aucun émoji proposé"</string>
     <string name="emoji_empty_recent_category" msgid="7863877827879290200">"Vous n\'avez encore utilisé aucun émoji"</string>
+    <!-- no translation found for emoji_bidirectional_switcher_content_desc (5084600168354220605) -->
+    <skip />
 </resources>
diff --git a/emoji2/emoji2-emojipicker/src/main/res/values-fr/strings.xml b/emoji2/emoji2-emojipicker/src/main/res/values-fr/strings.xml
index 57fb951..3cb7ac7 100644
--- a/emoji2/emoji2-emojipicker/src/main/res/values-fr/strings.xml
+++ b/emoji2/emoji2-emojipicker/src/main/res/values-fr/strings.xml
@@ -29,4 +29,6 @@
     <string name="emoji_category_flags" msgid="6185639503532784871">"DRAPEAUX"</string>
     <string name="emoji_empty_non_recent_category" msgid="288822832574892625">"Aucun emoji disponible"</string>
     <string name="emoji_empty_recent_category" msgid="7863877827879290200">"Vous n\'avez pas encore utilisé d\'emoji"</string>
+    <!-- no translation found for emoji_bidirectional_switcher_content_desc (5084600168354220605) -->
+    <skip />
 </resources>
diff --git a/emoji2/emoji2-emojipicker/src/main/res/values-gl/strings.xml b/emoji2/emoji2-emojipicker/src/main/res/values-gl/strings.xml
index 1330a9e..2bb0803 100644
--- a/emoji2/emoji2-emojipicker/src/main/res/values-gl/strings.xml
+++ b/emoji2/emoji2-emojipicker/src/main/res/values-gl/strings.xml
@@ -29,4 +29,6 @@
     <string name="emoji_category_flags" msgid="6185639503532784871">"BANDEIRAS"</string>
     <string name="emoji_empty_non_recent_category" msgid="288822832574892625">"Non hai ningún emoji dispoñible"</string>
     <string name="emoji_empty_recent_category" msgid="7863877827879290200">"Aínda non utilizaches ningún emoji"</string>
+    <!-- no translation found for emoji_bidirectional_switcher_content_desc (5084600168354220605) -->
+    <skip />
 </resources>
diff --git a/emoji2/emoji2-emojipicker/src/main/res/values-gu/strings.xml b/emoji2/emoji2-emojipicker/src/main/res/values-gu/strings.xml
index 3ba3807..a316e9f 100644
--- a/emoji2/emoji2-emojipicker/src/main/res/values-gu/strings.xml
+++ b/emoji2/emoji2-emojipicker/src/main/res/values-gu/strings.xml
@@ -29,4 +29,6 @@
     <string name="emoji_category_flags" msgid="6185639503532784871">"ઝંડા"</string>
     <string name="emoji_empty_non_recent_category" msgid="288822832574892625">"કોઈ ઇમોજી ઉપલબ્ધ નથી"</string>
     <string name="emoji_empty_recent_category" msgid="7863877827879290200">"તમે હજી સુધી કોઈ ઇમોજીનો ઉપયોગ કર્યો નથી"</string>
+    <!-- no translation found for emoji_bidirectional_switcher_content_desc (5084600168354220605) -->
+    <skip />
 </resources>
diff --git a/emoji2/emoji2-emojipicker/src/main/res/values-hi/strings.xml b/emoji2/emoji2-emojipicker/src/main/res/values-hi/strings.xml
index 9527a6a..a09653d 100644
--- a/emoji2/emoji2-emojipicker/src/main/res/values-hi/strings.xml
+++ b/emoji2/emoji2-emojipicker/src/main/res/values-hi/strings.xml
@@ -29,4 +29,6 @@
     <string name="emoji_category_flags" msgid="6185639503532784871">"झंडे"</string>
     <string name="emoji_empty_non_recent_category" msgid="288822832574892625">"कोई इमोजी उपलब्ध नहीं है"</string>
     <string name="emoji_empty_recent_category" msgid="7863877827879290200">"आपने अब तक किसी भी इमोजी का इस्तेमाल नहीं किया है"</string>
+    <!-- no translation found for emoji_bidirectional_switcher_content_desc (5084600168354220605) -->
+    <skip />
 </resources>
diff --git a/emoji2/emoji2-emojipicker/src/main/res/values-hr/strings.xml b/emoji2/emoji2-emojipicker/src/main/res/values-hr/strings.xml
index 3d59f54..d2ee647 100644
--- a/emoji2/emoji2-emojipicker/src/main/res/values-hr/strings.xml
+++ b/emoji2/emoji2-emojipicker/src/main/res/values-hr/strings.xml
@@ -29,4 +29,6 @@
     <string name="emoji_category_flags" msgid="6185639503532784871">"ZASTAVE"</string>
     <string name="emoji_empty_non_recent_category" msgid="288822832574892625">"Nije dostupan nijedan emoji"</string>
     <string name="emoji_empty_recent_category" msgid="7863877827879290200">"Još niste upotrijebili emojije"</string>
+    <!-- no translation found for emoji_bidirectional_switcher_content_desc (5084600168354220605) -->
+    <skip />
 </resources>
diff --git a/emoji2/emoji2-emojipicker/src/main/res/values-hu/strings.xml b/emoji2/emoji2-emojipicker/src/main/res/values-hu/strings.xml
index fcd1d30..3303bef 100644
--- a/emoji2/emoji2-emojipicker/src/main/res/values-hu/strings.xml
+++ b/emoji2/emoji2-emojipicker/src/main/res/values-hu/strings.xml
@@ -29,4 +29,6 @@
     <string name="emoji_category_flags" msgid="6185639503532784871">"ZÁSZLÓK"</string>
     <string name="emoji_empty_non_recent_category" msgid="288822832574892625">"Nincsenek rendelkezésre álló emojik"</string>
     <string name="emoji_empty_recent_category" msgid="7863877827879290200">"Még nem használt emojikat"</string>
+    <!-- no translation found for emoji_bidirectional_switcher_content_desc (5084600168354220605) -->
+    <skip />
 </resources>
diff --git a/emoji2/emoji2-emojipicker/src/main/res/values-hy/strings.xml b/emoji2/emoji2-emojipicker/src/main/res/values-hy/strings.xml
index 2be01fd..86852f1 100644
--- a/emoji2/emoji2-emojipicker/src/main/res/values-hy/strings.xml
+++ b/emoji2/emoji2-emojipicker/src/main/res/values-hy/strings.xml
@@ -29,4 +29,6 @@
     <string name="emoji_category_flags" msgid="6185639503532784871">"ԴՐՈՇՆԵՐ"</string>
     <string name="emoji_empty_non_recent_category" msgid="288822832574892625">"Հասանելի էմոջիներ չկան"</string>
     <string name="emoji_empty_recent_category" msgid="7863877827879290200">"Դուք դեռ չեք օգտագործել էմոջիներ"</string>
+    <!-- no translation found for emoji_bidirectional_switcher_content_desc (5084600168354220605) -->
+    <skip />
 </resources>
diff --git a/emoji2/emoji2-emojipicker/src/main/res/values-in/strings.xml b/emoji2/emoji2-emojipicker/src/main/res/values-in/strings.xml
index e942428..053a26f 100644
--- a/emoji2/emoji2-emojipicker/src/main/res/values-in/strings.xml
+++ b/emoji2/emoji2-emojipicker/src/main/res/values-in/strings.xml
@@ -29,4 +29,6 @@
     <string name="emoji_category_flags" msgid="6185639503532784871">"BENDERA"</string>
     <string name="emoji_empty_non_recent_category" msgid="288822832574892625">"Tidak ada emoji yang tersedia"</string>
     <string name="emoji_empty_recent_category" msgid="7863877827879290200">"Anda belum menggunakan emoji apa pun"</string>
+    <!-- no translation found for emoji_bidirectional_switcher_content_desc (5084600168354220605) -->
+    <skip />
 </resources>
diff --git a/emoji2/emoji2-emojipicker/src/main/res/values-is/strings.xml b/emoji2/emoji2-emojipicker/src/main/res/values-is/strings.xml
index c13a86c..8986e91 100644
--- a/emoji2/emoji2-emojipicker/src/main/res/values-is/strings.xml
+++ b/emoji2/emoji2-emojipicker/src/main/res/values-is/strings.xml
@@ -29,4 +29,6 @@
     <string name="emoji_category_flags" msgid="6185639503532784871">"FÁNAR"</string>
     <string name="emoji_empty_non_recent_category" msgid="288822832574892625">"Engin emoji-tákn í boði"</string>
     <string name="emoji_empty_recent_category" msgid="7863877827879290200">"Þú hefur ekki notað nein emoji enn"</string>
+    <!-- no translation found for emoji_bidirectional_switcher_content_desc (5084600168354220605) -->
+    <skip />
 </resources>
diff --git a/emoji2/emoji2-emojipicker/src/main/res/values-it/strings.xml b/emoji2/emoji2-emojipicker/src/main/res/values-it/strings.xml
index 20dacd9..4307ffd 100644
--- a/emoji2/emoji2-emojipicker/src/main/res/values-it/strings.xml
+++ b/emoji2/emoji2-emojipicker/src/main/res/values-it/strings.xml
@@ -29,4 +29,6 @@
     <string name="emoji_category_flags" msgid="6185639503532784871">"BANDIERE"</string>
     <string name="emoji_empty_non_recent_category" msgid="288822832574892625">"Nessuna emoji disponibile"</string>
     <string name="emoji_empty_recent_category" msgid="7863877827879290200">"Non hai ancora usato alcuna emoji"</string>
+    <!-- no translation found for emoji_bidirectional_switcher_content_desc (5084600168354220605) -->
+    <skip />
 </resources>
diff --git a/emoji2/emoji2-emojipicker/src/main/res/values-iw/strings.xml b/emoji2/emoji2-emojipicker/src/main/res/values-iw/strings.xml
index 069d4bf..e155d4c 100644
--- a/emoji2/emoji2-emojipicker/src/main/res/values-iw/strings.xml
+++ b/emoji2/emoji2-emojipicker/src/main/res/values-iw/strings.xml
@@ -29,4 +29,6 @@
     <string name="emoji_category_flags" msgid="6185639503532784871">"דגלים"</string>
     <string name="emoji_empty_non_recent_category" msgid="288822832574892625">"אין סמלי אמוג\'י זמינים"</string>
     <string name="emoji_empty_recent_category" msgid="7863877827879290200">"עדיין לא השתמשת באף אמוג\'י"</string>
+    <!-- no translation found for emoji_bidirectional_switcher_content_desc (5084600168354220605) -->
+    <skip />
 </resources>
diff --git a/emoji2/emoji2-emojipicker/src/main/res/values-ja/strings.xml b/emoji2/emoji2-emojipicker/src/main/res/values-ja/strings.xml
index 8d6f69b..63094ef 100644
--- a/emoji2/emoji2-emojipicker/src/main/res/values-ja/strings.xml
+++ b/emoji2/emoji2-emojipicker/src/main/res/values-ja/strings.xml
@@ -29,4 +29,6 @@
     <string name="emoji_category_flags" msgid="6185639503532784871">"旗"</string>
     <string name="emoji_empty_non_recent_category" msgid="288822832574892625">"使用できる絵文字がありません"</string>
     <string name="emoji_empty_recent_category" msgid="7863877827879290200">"まだ絵文字を使用していません"</string>
+    <!-- no translation found for emoji_bidirectional_switcher_content_desc (5084600168354220605) -->
+    <skip />
 </resources>
diff --git a/emoji2/emoji2-emojipicker/src/main/res/values-ka/strings.xml b/emoji2/emoji2-emojipicker/src/main/res/values-ka/strings.xml
index e256126..3662c4f 100644
--- a/emoji2/emoji2-emojipicker/src/main/res/values-ka/strings.xml
+++ b/emoji2/emoji2-emojipicker/src/main/res/values-ka/strings.xml
@@ -29,4 +29,6 @@
     <string name="emoji_category_flags" msgid="6185639503532784871">"დროშები"</string>
     <string name="emoji_empty_non_recent_category" msgid="288822832574892625">"Emoji-ები მიუწვდომელია"</string>
     <string name="emoji_empty_recent_category" msgid="7863877827879290200">"Emoji-ებით ჯერ არ გისარგებლიათ"</string>
+    <!-- no translation found for emoji_bidirectional_switcher_content_desc (5084600168354220605) -->
+    <skip />
 </resources>
diff --git a/emoji2/emoji2-emojipicker/src/main/res/values-kk/strings.xml b/emoji2/emoji2-emojipicker/src/main/res/values-kk/strings.xml
index 7537ddb..f172f31 100644
--- a/emoji2/emoji2-emojipicker/src/main/res/values-kk/strings.xml
+++ b/emoji2/emoji2-emojipicker/src/main/res/values-kk/strings.xml
@@ -29,4 +29,6 @@
     <string name="emoji_category_flags" msgid="6185639503532784871">"ЖАЛАУШАЛАР"</string>
     <string name="emoji_empty_non_recent_category" msgid="288822832574892625">"Эмоджи жоқ"</string>
     <string name="emoji_empty_recent_category" msgid="7863877827879290200">"Әлі ешқандай эмоджи пайдаланылған жоқ."</string>
+    <!-- no translation found for emoji_bidirectional_switcher_content_desc (5084600168354220605) -->
+    <skip />
 </resources>
diff --git a/emoji2/emoji2-emojipicker/src/main/res/values-km/strings.xml b/emoji2/emoji2-emojipicker/src/main/res/values-km/strings.xml
index a8a57f4..5b3c2e4 100644
--- a/emoji2/emoji2-emojipicker/src/main/res/values-km/strings.xml
+++ b/emoji2/emoji2-emojipicker/src/main/res/values-km/strings.xml
@@ -29,4 +29,6 @@
     <string name="emoji_category_flags" msgid="6185639503532784871">"ទង់"</string>
     <string name="emoji_empty_non_recent_category" msgid="288822832574892625">"មិនមាន​រូប​អារម្មណ៍ទេ"</string>
     <string name="emoji_empty_recent_category" msgid="7863877827879290200">"អ្នក​មិនទាន់​បានប្រើរូប​អារម្មណ៍​ណាមួយ​នៅឡើយទេ"</string>
+    <!-- no translation found for emoji_bidirectional_switcher_content_desc (5084600168354220605) -->
+    <skip />
 </resources>
diff --git a/emoji2/emoji2-emojipicker/src/main/res/values-kn/strings.xml b/emoji2/emoji2-emojipicker/src/main/res/values-kn/strings.xml
index ff09e10..c36e87d 100644
--- a/emoji2/emoji2-emojipicker/src/main/res/values-kn/strings.xml
+++ b/emoji2/emoji2-emojipicker/src/main/res/values-kn/strings.xml
@@ -29,4 +29,6 @@
     <string name="emoji_category_flags" msgid="6185639503532784871">"ಫ್ಲ್ಯಾಗ್‌ಗಳು"</string>
     <string name="emoji_empty_non_recent_category" msgid="288822832574892625">"ಯಾವುದೇ ಎಮೊಜಿಗಳು ಲಭ್ಯವಿಲ್ಲ"</string>
     <string name="emoji_empty_recent_category" msgid="7863877827879290200">"ನೀವು ಇನ್ನೂ ಯಾವುದೇ ಎಮೋಜಿಗಳನ್ನು ಬಳಸಿಲ್ಲ"</string>
+    <!-- no translation found for emoji_bidirectional_switcher_content_desc (5084600168354220605) -->
+    <skip />
 </resources>
diff --git a/emoji2/emoji2-emojipicker/src/main/res/values-ko/strings.xml b/emoji2/emoji2-emojipicker/src/main/res/values-ko/strings.xml
index d5e4d35..2ac1df7 100644
--- a/emoji2/emoji2-emojipicker/src/main/res/values-ko/strings.xml
+++ b/emoji2/emoji2-emojipicker/src/main/res/values-ko/strings.xml
@@ -29,4 +29,6 @@
     <string name="emoji_category_flags" msgid="6185639503532784871">"깃발"</string>
     <string name="emoji_empty_non_recent_category" msgid="288822832574892625">"사용 가능한 그림 이모티콘 없음"</string>
     <string name="emoji_empty_recent_category" msgid="7863877827879290200">"아직 사용한 이모티콘이 없습니다."</string>
+    <!-- no translation found for emoji_bidirectional_switcher_content_desc (5084600168354220605) -->
+    <skip />
 </resources>
diff --git a/emoji2/emoji2-emojipicker/src/main/res/values-ky/strings.xml b/emoji2/emoji2-emojipicker/src/main/res/values-ky/strings.xml
index c134949..2ca75f5b 100644
--- a/emoji2/emoji2-emojipicker/src/main/res/values-ky/strings.xml
+++ b/emoji2/emoji2-emojipicker/src/main/res/values-ky/strings.xml
@@ -29,4 +29,6 @@
     <string name="emoji_category_flags" msgid="6185639503532784871">"ЖЕЛЕКТЕР"</string>
     <string name="emoji_empty_non_recent_category" msgid="288822832574892625">"Жеткиликтүү быйтыкчалар жок"</string>
     <string name="emoji_empty_recent_category" msgid="7863877827879290200">"Бир да быйтыкча колдоно элексиз"</string>
+    <!-- no translation found for emoji_bidirectional_switcher_content_desc (5084600168354220605) -->
+    <skip />
 </resources>
diff --git a/emoji2/emoji2-emojipicker/src/main/res/values-lo/strings.xml b/emoji2/emoji2-emojipicker/src/main/res/values-lo/strings.xml
index 8392941..71da66c 100644
--- a/emoji2/emoji2-emojipicker/src/main/res/values-lo/strings.xml
+++ b/emoji2/emoji2-emojipicker/src/main/res/values-lo/strings.xml
@@ -29,4 +29,6 @@
     <string name="emoji_category_flags" msgid="6185639503532784871">"ທຸງ"</string>
     <string name="emoji_empty_non_recent_category" msgid="288822832574892625">"ບໍ່ມີອີໂມຈິໃຫ້ນຳໃຊ້"</string>
     <string name="emoji_empty_recent_category" msgid="7863877827879290200">"ທ່ານຍັງບໍ່ໄດ້ໃຊ້ອີໂມຈິໃດເທື່ອ"</string>
+    <!-- no translation found for emoji_bidirectional_switcher_content_desc (5084600168354220605) -->
+    <skip />
 </resources>
diff --git a/emoji2/emoji2-emojipicker/src/main/res/values-lt/strings.xml b/emoji2/emoji2-emojipicker/src/main/res/values-lt/strings.xml
index d4b99f1..5ab16c7 100644
--- a/emoji2/emoji2-emojipicker/src/main/res/values-lt/strings.xml
+++ b/emoji2/emoji2-emojipicker/src/main/res/values-lt/strings.xml
@@ -29,4 +29,6 @@
     <string name="emoji_category_flags" msgid="6185639503532784871">"VĖLIAVOS"</string>
     <string name="emoji_empty_non_recent_category" msgid="288822832574892625">"Nėra jokių pasiekiamų jaustukų"</string>
     <string name="emoji_empty_recent_category" msgid="7863877827879290200">"Dar nenaudojote jokių jaustukų"</string>
+    <!-- no translation found for emoji_bidirectional_switcher_content_desc (5084600168354220605) -->
+    <skip />
 </resources>
diff --git a/emoji2/emoji2-emojipicker/src/main/res/values-lv/strings.xml b/emoji2/emoji2-emojipicker/src/main/res/values-lv/strings.xml
index 33a7097..b163279 100644
--- a/emoji2/emoji2-emojipicker/src/main/res/values-lv/strings.xml
+++ b/emoji2/emoji2-emojipicker/src/main/res/values-lv/strings.xml
@@ -29,4 +29,6 @@
     <string name="emoji_category_flags" msgid="6185639503532784871">"KAROGI"</string>
     <string name="emoji_empty_non_recent_category" msgid="288822832574892625">"Nav pieejamu emocijzīmju"</string>
     <string name="emoji_empty_recent_category" msgid="7863877827879290200">"Jūs vēl neesat izmantojis nevienu emocijzīmi"</string>
+    <!-- no translation found for emoji_bidirectional_switcher_content_desc (5084600168354220605) -->
+    <skip />
 </resources>
diff --git a/emoji2/emoji2-emojipicker/src/main/res/values-mk/strings.xml b/emoji2/emoji2-emojipicker/src/main/res/values-mk/strings.xml
index 5216964..c65fcfb 100644
--- a/emoji2/emoji2-emojipicker/src/main/res/values-mk/strings.xml
+++ b/emoji2/emoji2-emojipicker/src/main/res/values-mk/strings.xml
@@ -29,4 +29,6 @@
     <string name="emoji_category_flags" msgid="6185639503532784871">"ЗНАМИЊА"</string>
     <string name="emoji_empty_non_recent_category" msgid="288822832574892625">"Нема достапни емоџија"</string>
     <string name="emoji_empty_recent_category" msgid="7863877827879290200">"Сѐ уште не сте користеле емоџија"</string>
+    <!-- no translation found for emoji_bidirectional_switcher_content_desc (5084600168354220605) -->
+    <skip />
 </resources>
diff --git a/emoji2/emoji2-emojipicker/src/main/res/values-ml/strings.xml b/emoji2/emoji2-emojipicker/src/main/res/values-ml/strings.xml
index 3c54aa0..327d8a0 100644
--- a/emoji2/emoji2-emojipicker/src/main/res/values-ml/strings.xml
+++ b/emoji2/emoji2-emojipicker/src/main/res/values-ml/strings.xml
@@ -29,4 +29,6 @@
     <string name="emoji_category_flags" msgid="6185639503532784871">"പതാകകൾ"</string>
     <string name="emoji_empty_non_recent_category" msgid="288822832574892625">"ഇമോജികളൊന്നും ലഭ്യമല്ല"</string>
     <string name="emoji_empty_recent_category" msgid="7863877827879290200">"നിങ്ങൾ ഇതുവരെ ഇമോജികളൊന്നും ഉപയോഗിച്ചിട്ടില്ല"</string>
+    <!-- no translation found for emoji_bidirectional_switcher_content_desc (5084600168354220605) -->
+    <skip />
 </resources>
diff --git a/emoji2/emoji2-emojipicker/src/main/res/values-mn/strings.xml b/emoji2/emoji2-emojipicker/src/main/res/values-mn/strings.xml
index 8fee383..f36da6c 100644
--- a/emoji2/emoji2-emojipicker/src/main/res/values-mn/strings.xml
+++ b/emoji2/emoji2-emojipicker/src/main/res/values-mn/strings.xml
@@ -29,4 +29,6 @@
     <string name="emoji_category_flags" msgid="6185639503532784871">"ТУГ"</string>
     <string name="emoji_empty_non_recent_category" msgid="288822832574892625">"Боломжтой эможи алга"</string>
     <string name="emoji_empty_recent_category" msgid="7863877827879290200">"Та ямар нэгэн эможи ашиглаагүй байна"</string>
+    <!-- no translation found for emoji_bidirectional_switcher_content_desc (5084600168354220605) -->
+    <skip />
 </resources>
diff --git a/emoji2/emoji2-emojipicker/src/main/res/values-mr/strings.xml b/emoji2/emoji2-emojipicker/src/main/res/values-mr/strings.xml
index 2c4584f..10853bd4 100644
--- a/emoji2/emoji2-emojipicker/src/main/res/values-mr/strings.xml
+++ b/emoji2/emoji2-emojipicker/src/main/res/values-mr/strings.xml
@@ -29,4 +29,6 @@
     <string name="emoji_category_flags" msgid="6185639503532784871">"ध्वज"</string>
     <string name="emoji_empty_non_recent_category" msgid="288822832574892625">"कोणतेही इमोजी उपलब्ध नाहीत"</string>
     <string name="emoji_empty_recent_category" msgid="7863877827879290200">"तुम्ही अद्याप कोणतेही इमोजी वापरलेले नाहीत"</string>
+    <!-- no translation found for emoji_bidirectional_switcher_content_desc (5084600168354220605) -->
+    <skip />
 </resources>
diff --git a/emoji2/emoji2-emojipicker/src/main/res/values-ms/strings.xml b/emoji2/emoji2-emojipicker/src/main/res/values-ms/strings.xml
index c30d0ee..7153b7b 100644
--- a/emoji2/emoji2-emojipicker/src/main/res/values-ms/strings.xml
+++ b/emoji2/emoji2-emojipicker/src/main/res/values-ms/strings.xml
@@ -29,4 +29,6 @@
     <string name="emoji_category_flags" msgid="6185639503532784871">"BENDERA"</string>
     <string name="emoji_empty_non_recent_category" msgid="288822832574892625">"Tiada emoji tersedia"</string>
     <string name="emoji_empty_recent_category" msgid="7863877827879290200">"Anda belum menggunakan mana-mana emoji lagi"</string>
+    <!-- no translation found for emoji_bidirectional_switcher_content_desc (5084600168354220605) -->
+    <skip />
 </resources>
diff --git a/emoji2/emoji2-emojipicker/src/main/res/values-my/strings.xml b/emoji2/emoji2-emojipicker/src/main/res/values-my/strings.xml
index f4f4239..0ebe3f6 100644
--- a/emoji2/emoji2-emojipicker/src/main/res/values-my/strings.xml
+++ b/emoji2/emoji2-emojipicker/src/main/res/values-my/strings.xml
@@ -29,4 +29,6 @@
     <string name="emoji_category_flags" msgid="6185639503532784871">"အလံများ"</string>
     <string name="emoji_empty_non_recent_category" msgid="288822832574892625">"အီမိုဂျီ မရနိုင်ပါ"</string>
     <string name="emoji_empty_recent_category" msgid="7863877827879290200">"အီမိုဂျီ အသုံးမပြုသေးပါ"</string>
+    <!-- no translation found for emoji_bidirectional_switcher_content_desc (5084600168354220605) -->
+    <skip />
 </resources>
diff --git a/emoji2/emoji2-emojipicker/src/main/res/values-nb/strings.xml b/emoji2/emoji2-emojipicker/src/main/res/values-nb/strings.xml
index 3f3f2af..909485c 100644
--- a/emoji2/emoji2-emojipicker/src/main/res/values-nb/strings.xml
+++ b/emoji2/emoji2-emojipicker/src/main/res/values-nb/strings.xml
@@ -29,4 +29,6 @@
     <string name="emoji_category_flags" msgid="6185639503532784871">"FLAGG"</string>
     <string name="emoji_empty_non_recent_category" msgid="288822832574892625">"Ingen emojier er tilgjengelige"</string>
     <string name="emoji_empty_recent_category" msgid="7863877827879290200">"Du har ikke brukt noen emojier ennå"</string>
+    <!-- no translation found for emoji_bidirectional_switcher_content_desc (5084600168354220605) -->
+    <skip />
 </resources>
diff --git a/emoji2/emoji2-emojipicker/src/main/res/values-ne/strings.xml b/emoji2/emoji2-emojipicker/src/main/res/values-ne/strings.xml
index 559a47f..6fdffd0 100644
--- a/emoji2/emoji2-emojipicker/src/main/res/values-ne/strings.xml
+++ b/emoji2/emoji2-emojipicker/src/main/res/values-ne/strings.xml
@@ -29,4 +29,6 @@
     <string name="emoji_category_flags" msgid="6185639503532784871">"झन्डाहरू"</string>
     <string name="emoji_empty_non_recent_category" msgid="288822832574892625">"कुनै पनि इमोजी उपलब्ध छैन"</string>
     <string name="emoji_empty_recent_category" msgid="7863877827879290200">"तपाईंले हालसम्म कुनै पनि इमोजी प्रयोग गर्नुभएको छैन"</string>
+    <!-- no translation found for emoji_bidirectional_switcher_content_desc (5084600168354220605) -->
+    <skip />
 </resources>
diff --git a/emoji2/emoji2-emojipicker/src/main/res/values-nl/strings.xml b/emoji2/emoji2-emojipicker/src/main/res/values-nl/strings.xml
index a86b322..c9b3e3c 100644
--- a/emoji2/emoji2-emojipicker/src/main/res/values-nl/strings.xml
+++ b/emoji2/emoji2-emojipicker/src/main/res/values-nl/strings.xml
@@ -29,4 +29,6 @@
     <string name="emoji_category_flags" msgid="6185639503532784871">"VLAGGEN"</string>
     <string name="emoji_empty_non_recent_category" msgid="288822832574892625">"Geen emoji\'s beschikbaar"</string>
     <string name="emoji_empty_recent_category" msgid="7863877827879290200">"Je hebt nog geen emoji\'s gebruikt"</string>
+    <!-- no translation found for emoji_bidirectional_switcher_content_desc (5084600168354220605) -->
+    <skip />
 </resources>
diff --git a/emoji2/emoji2-emojipicker/src/main/res/values-or/strings.xml b/emoji2/emoji2-emojipicker/src/main/res/values-or/strings.xml
index b2609fe..55ba407 100644
--- a/emoji2/emoji2-emojipicker/src/main/res/values-or/strings.xml
+++ b/emoji2/emoji2-emojipicker/src/main/res/values-or/strings.xml
@@ -29,4 +29,6 @@
     <string name="emoji_category_flags" msgid="6185639503532784871">"ଫ୍ଲାଗଗୁଡ଼ିକ"</string>
     <string name="emoji_empty_non_recent_category" msgid="288822832574892625">"କୌଣସି ଇମୋଜି ଉପଲବ୍ଧ ନାହିଁ"</string>
     <string name="emoji_empty_recent_category" msgid="7863877827879290200">"ଆପଣ ଏପର୍ଯ୍ୟନ୍ତ କୌଣସି ଇମୋଜି ବ୍ୟବହାର କରିନାହାଁନ୍ତି"</string>
+    <!-- no translation found for emoji_bidirectional_switcher_content_desc (5084600168354220605) -->
+    <skip />
 </resources>
diff --git a/emoji2/emoji2-emojipicker/src/main/res/values-pa/strings.xml b/emoji2/emoji2-emojipicker/src/main/res/values-pa/strings.xml
index 661dce8..f9ddc80 100644
--- a/emoji2/emoji2-emojipicker/src/main/res/values-pa/strings.xml
+++ b/emoji2/emoji2-emojipicker/src/main/res/values-pa/strings.xml
@@ -29,4 +29,6 @@
     <string name="emoji_category_flags" msgid="6185639503532784871">"ਝੰਡੇ"</string>
     <string name="emoji_empty_non_recent_category" msgid="288822832574892625">"ਕੋਈ ਇਮੋਜੀ ਉਪਲਬਧ ਨਹੀਂ ਹੈ"</string>
     <string name="emoji_empty_recent_category" msgid="7863877827879290200">"ਤੁਸੀਂ ਹਾਲੇ ਤੱਕ ਕਿਸੇ ਵੀ ਇਮੋਜੀ ਦੀ ਵਰਤੋਂ ਨਹੀਂ ਕੀਤੀ ਹੈ"</string>
+    <!-- no translation found for emoji_bidirectional_switcher_content_desc (5084600168354220605) -->
+    <skip />
 </resources>
diff --git a/emoji2/emoji2-emojipicker/src/main/res/values-pl/strings.xml b/emoji2/emoji2-emojipicker/src/main/res/values-pl/strings.xml
index 3ec6876..cc87d09 100644
--- a/emoji2/emoji2-emojipicker/src/main/res/values-pl/strings.xml
+++ b/emoji2/emoji2-emojipicker/src/main/res/values-pl/strings.xml
@@ -29,4 +29,6 @@
     <string name="emoji_category_flags" msgid="6185639503532784871">"FLAGI"</string>
     <string name="emoji_empty_non_recent_category" msgid="288822832574892625">"Brak dostępnych emotikonów"</string>
     <string name="emoji_empty_recent_category" msgid="7863877827879290200">"Żadne emotikony nie zostały jeszcze użyte"</string>
+    <!-- no translation found for emoji_bidirectional_switcher_content_desc (5084600168354220605) -->
+    <skip />
 </resources>
diff --git a/emoji2/emoji2-emojipicker/src/main/res/values-pt-rBR/strings.xml b/emoji2/emoji2-emojipicker/src/main/res/values-pt-rBR/strings.xml
index 69c3ff5..14187a3 100644
--- a/emoji2/emoji2-emojipicker/src/main/res/values-pt-rBR/strings.xml
+++ b/emoji2/emoji2-emojipicker/src/main/res/values-pt-rBR/strings.xml
@@ -29,4 +29,6 @@
     <string name="emoji_category_flags" msgid="6185639503532784871">"BANDEIRAS"</string>
     <string name="emoji_empty_non_recent_category" msgid="288822832574892625">"Não há emojis disponíveis"</string>
     <string name="emoji_empty_recent_category" msgid="7863877827879290200">"Você ainda não usou emojis"</string>
+    <!-- no translation found for emoji_bidirectional_switcher_content_desc (5084600168354220605) -->
+    <skip />
 </resources>
diff --git a/emoji2/emoji2-emojipicker/src/main/res/values-pt-rPT/strings.xml b/emoji2/emoji2-emojipicker/src/main/res/values-pt-rPT/strings.xml
index cd448a3..8af463d 100644
--- a/emoji2/emoji2-emojipicker/src/main/res/values-pt-rPT/strings.xml
+++ b/emoji2/emoji2-emojipicker/src/main/res/values-pt-rPT/strings.xml
@@ -29,4 +29,6 @@
     <string name="emoji_category_flags" msgid="6185639503532784871">"BANDEIRAS"</string>
     <string name="emoji_empty_non_recent_category" msgid="288822832574892625">"Nenhum emoji disponível"</string>
     <string name="emoji_empty_recent_category" msgid="7863877827879290200">"Ainda não utilizou emojis"</string>
+    <!-- no translation found for emoji_bidirectional_switcher_content_desc (5084600168354220605) -->
+    <skip />
 </resources>
diff --git a/emoji2/emoji2-emojipicker/src/main/res/values-pt/strings.xml b/emoji2/emoji2-emojipicker/src/main/res/values-pt/strings.xml
index 69c3ff5..14187a3 100644
--- a/emoji2/emoji2-emojipicker/src/main/res/values-pt/strings.xml
+++ b/emoji2/emoji2-emojipicker/src/main/res/values-pt/strings.xml
@@ -29,4 +29,6 @@
     <string name="emoji_category_flags" msgid="6185639503532784871">"BANDEIRAS"</string>
     <string name="emoji_empty_non_recent_category" msgid="288822832574892625">"Não há emojis disponíveis"</string>
     <string name="emoji_empty_recent_category" msgid="7863877827879290200">"Você ainda não usou emojis"</string>
+    <!-- no translation found for emoji_bidirectional_switcher_content_desc (5084600168354220605) -->
+    <skip />
 </resources>
diff --git a/emoji2/emoji2-emojipicker/src/main/res/values-ro/strings.xml b/emoji2/emoji2-emojipicker/src/main/res/values-ro/strings.xml
index 6365913..4491530 100644
--- a/emoji2/emoji2-emojipicker/src/main/res/values-ro/strings.xml
+++ b/emoji2/emoji2-emojipicker/src/main/res/values-ro/strings.xml
@@ -29,4 +29,6 @@
     <string name="emoji_category_flags" msgid="6185639503532784871">"STEAGURI"</string>
     <string name="emoji_empty_non_recent_category" msgid="288822832574892625">"Nu sunt disponibile emoji-uri"</string>
     <string name="emoji_empty_recent_category" msgid="7863877827879290200">"Încă nu ai folosit emoji"</string>
+    <!-- no translation found for emoji_bidirectional_switcher_content_desc (5084600168354220605) -->
+    <skip />
 </resources>
diff --git a/emoji2/emoji2-emojipicker/src/main/res/values-ru/strings.xml b/emoji2/emoji2-emojipicker/src/main/res/values-ru/strings.xml
index e4d406d..79317ee 100644
--- a/emoji2/emoji2-emojipicker/src/main/res/values-ru/strings.xml
+++ b/emoji2/emoji2-emojipicker/src/main/res/values-ru/strings.xml
@@ -29,4 +29,6 @@
     <string name="emoji_category_flags" msgid="6185639503532784871">"ФЛАГИ"</string>
     <string name="emoji_empty_non_recent_category" msgid="288822832574892625">"Нет доступных эмодзи"</string>
     <string name="emoji_empty_recent_category" msgid="7863877827879290200">"Вы ещё не использовали эмодзи"</string>
+    <!-- no translation found for emoji_bidirectional_switcher_content_desc (5084600168354220605) -->
+    <skip />
 </resources>
diff --git a/emoji2/emoji2-emojipicker/src/main/res/values-si/strings.xml b/emoji2/emoji2-emojipicker/src/main/res/values-si/strings.xml
index e00199a..b451d73 100644
--- a/emoji2/emoji2-emojipicker/src/main/res/values-si/strings.xml
+++ b/emoji2/emoji2-emojipicker/src/main/res/values-si/strings.xml
@@ -29,4 +29,6 @@
     <string name="emoji_category_flags" msgid="6185639503532784871">"ධජ"</string>
     <string name="emoji_empty_non_recent_category" msgid="288822832574892625">"ඉමොජි කිසිවක් නොලැබේ"</string>
     <string name="emoji_empty_recent_category" msgid="7863877827879290200">"ඔබ තවමත් කිසිදු ඉමෝජියක් භාවිතා කර නැත"</string>
+    <!-- no translation found for emoji_bidirectional_switcher_content_desc (5084600168354220605) -->
+    <skip />
 </resources>
diff --git a/emoji2/emoji2-emojipicker/src/main/res/values-sk/strings.xml b/emoji2/emoji2-emojipicker/src/main/res/values-sk/strings.xml
index 57e00cd..c4e79a5 100644
--- a/emoji2/emoji2-emojipicker/src/main/res/values-sk/strings.xml
+++ b/emoji2/emoji2-emojipicker/src/main/res/values-sk/strings.xml
@@ -29,4 +29,6 @@
     <string name="emoji_category_flags" msgid="6185639503532784871">"VLAJKY"</string>
     <string name="emoji_empty_non_recent_category" msgid="288822832574892625">"Nie sú k dispozícii žiadne emodži"</string>
     <string name="emoji_empty_recent_category" msgid="7863877827879290200">"Zatiaľ ste nepoužili žiadne emodži"</string>
+    <!-- no translation found for emoji_bidirectional_switcher_content_desc (5084600168354220605) -->
+    <skip />
 </resources>
diff --git a/emoji2/emoji2-emojipicker/src/main/res/values-sl/strings.xml b/emoji2/emoji2-emojipicker/src/main/res/values-sl/strings.xml
index addf0c7..a104b96 100644
--- a/emoji2/emoji2-emojipicker/src/main/res/values-sl/strings.xml
+++ b/emoji2/emoji2-emojipicker/src/main/res/values-sl/strings.xml
@@ -29,4 +29,6 @@
     <string name="emoji_category_flags" msgid="6185639503532784871">"ZASTAVE"</string>
     <string name="emoji_empty_non_recent_category" msgid="288822832574892625">"Ni emodžijev"</string>
     <string name="emoji_empty_recent_category" msgid="7863877827879290200">"Uporabili niste še nobenega emodžija."</string>
+    <!-- no translation found for emoji_bidirectional_switcher_content_desc (5084600168354220605) -->
+    <skip />
 </resources>
diff --git a/emoji2/emoji2-emojipicker/src/main/res/values-sq/strings.xml b/emoji2/emoji2-emojipicker/src/main/res/values-sq/strings.xml
index 64b2631..c1d871b 100644
--- a/emoji2/emoji2-emojipicker/src/main/res/values-sq/strings.xml
+++ b/emoji2/emoji2-emojipicker/src/main/res/values-sq/strings.xml
@@ -29,4 +29,6 @@
     <string name="emoji_category_flags" msgid="6185639503532784871">"FLAMUJ"</string>
     <string name="emoji_empty_non_recent_category" msgid="288822832574892625">"Nuk ofrohen emoji"</string>
     <string name="emoji_empty_recent_category" msgid="7863877827879290200">"Nuk ke përdorur ende asnjë emoji"</string>
+    <!-- no translation found for emoji_bidirectional_switcher_content_desc (5084600168354220605) -->
+    <skip />
 </resources>
diff --git a/emoji2/emoji2-emojipicker/src/main/res/values-sr/strings.xml b/emoji2/emoji2-emojipicker/src/main/res/values-sr/strings.xml
index 52a8c9b..a85908b 100644
--- a/emoji2/emoji2-emojipicker/src/main/res/values-sr/strings.xml
+++ b/emoji2/emoji2-emojipicker/src/main/res/values-sr/strings.xml
@@ -29,4 +29,6 @@
     <string name="emoji_category_flags" msgid="6185639503532784871">"ЗАСТАВЕ"</string>
     <string name="emoji_empty_non_recent_category" msgid="288822832574892625">"Емоџији нису доступни"</string>
     <string name="emoji_empty_recent_category" msgid="7863877827879290200">"Још нисте користили емоџије"</string>
+    <!-- no translation found for emoji_bidirectional_switcher_content_desc (5084600168354220605) -->
+    <skip />
 </resources>
diff --git a/emoji2/emoji2-emojipicker/src/main/res/values-sv/strings.xml b/emoji2/emoji2-emojipicker/src/main/res/values-sv/strings.xml
index 38fece4..4fb6a64 100644
--- a/emoji2/emoji2-emojipicker/src/main/res/values-sv/strings.xml
+++ b/emoji2/emoji2-emojipicker/src/main/res/values-sv/strings.xml
@@ -29,4 +29,6 @@
     <string name="emoji_category_flags" msgid="6185639503532784871">"FLAGGOR"</string>
     <string name="emoji_empty_non_recent_category" msgid="288822832574892625">"Inga emojier tillgängliga"</string>
     <string name="emoji_empty_recent_category" msgid="7863877827879290200">"Du har ännu inte använt emojis"</string>
+    <!-- no translation found for emoji_bidirectional_switcher_content_desc (5084600168354220605) -->
+    <skip />
 </resources>
diff --git a/emoji2/emoji2-emojipicker/src/main/res/values-sw/strings.xml b/emoji2/emoji2-emojipicker/src/main/res/values-sw/strings.xml
index bd5436b..6a95579 100644
--- a/emoji2/emoji2-emojipicker/src/main/res/values-sw/strings.xml
+++ b/emoji2/emoji2-emojipicker/src/main/res/values-sw/strings.xml
@@ -29,4 +29,6 @@
     <string name="emoji_category_flags" msgid="6185639503532784871">"BENDERA"</string>
     <string name="emoji_empty_non_recent_category" msgid="288822832574892625">"Hakuna emoji zinazopatikana"</string>
     <string name="emoji_empty_recent_category" msgid="7863877827879290200">"Bado hujatumia emoji zozote"</string>
+    <!-- no translation found for emoji_bidirectional_switcher_content_desc (5084600168354220605) -->
+    <skip />
 </resources>
diff --git a/emoji2/emoji2-emojipicker/src/main/res/values-ta/strings.xml b/emoji2/emoji2-emojipicker/src/main/res/values-ta/strings.xml
index f2c6053..80a6dca 100644
--- a/emoji2/emoji2-emojipicker/src/main/res/values-ta/strings.xml
+++ b/emoji2/emoji2-emojipicker/src/main/res/values-ta/strings.xml
@@ -29,4 +29,6 @@
     <string name="emoji_category_flags" msgid="6185639503532784871">"கொடிகள்"</string>
     <string name="emoji_empty_non_recent_category" msgid="288822832574892625">"ஈமோஜிகள் எதுவுமில்லை"</string>
     <string name="emoji_empty_recent_category" msgid="7863877827879290200">"இதுவரை ஈமோஜி எதையும் நீங்கள் பயன்படுத்தவில்லை"</string>
+    <!-- no translation found for emoji_bidirectional_switcher_content_desc (5084600168354220605) -->
+    <skip />
 </resources>
diff --git a/emoji2/emoji2-emojipicker/src/main/res/values-te/strings.xml b/emoji2/emoji2-emojipicker/src/main/res/values-te/strings.xml
index 22d722f..f872dc9 100644
--- a/emoji2/emoji2-emojipicker/src/main/res/values-te/strings.xml
+++ b/emoji2/emoji2-emojipicker/src/main/res/values-te/strings.xml
@@ -29,4 +29,6 @@
     <string name="emoji_category_flags" msgid="6185639503532784871">"ఫ్లాగ్‌లు"</string>
     <string name="emoji_empty_non_recent_category" msgid="288822832574892625">"ఎమోజీలు ఏవీ అందుబాటులో లేవు"</string>
     <string name="emoji_empty_recent_category" msgid="7863877827879290200">"మీరు ఇంకా ఎమోజీలు ఏవీ ఉపయోగించలేదు"</string>
+    <!-- no translation found for emoji_bidirectional_switcher_content_desc (5084600168354220605) -->
+    <skip />
 </resources>
diff --git a/emoji2/emoji2-emojipicker/src/main/res/values-th/strings.xml b/emoji2/emoji2-emojipicker/src/main/res/values-th/strings.xml
index 9118893..31ea744 100644
--- a/emoji2/emoji2-emojipicker/src/main/res/values-th/strings.xml
+++ b/emoji2/emoji2-emojipicker/src/main/res/values-th/strings.xml
@@ -29,4 +29,6 @@
     <string name="emoji_category_flags" msgid="6185639503532784871">"ธง"</string>
     <string name="emoji_empty_non_recent_category" msgid="288822832574892625">"ไม่มีอีโมจิ"</string>
     <string name="emoji_empty_recent_category" msgid="7863877827879290200">"คุณยังไม่ได้ใช้อีโมจิเลย"</string>
+    <!-- no translation found for emoji_bidirectional_switcher_content_desc (5084600168354220605) -->
+    <skip />
 </resources>
diff --git a/emoji2/emoji2-emojipicker/src/main/res/values-tl/strings.xml b/emoji2/emoji2-emojipicker/src/main/res/values-tl/strings.xml
index 318025e..7216dbb 100644
--- a/emoji2/emoji2-emojipicker/src/main/res/values-tl/strings.xml
+++ b/emoji2/emoji2-emojipicker/src/main/res/values-tl/strings.xml
@@ -29,4 +29,6 @@
     <string name="emoji_category_flags" msgid="6185639503532784871">"MGA BANDILA"</string>
     <string name="emoji_empty_non_recent_category" msgid="288822832574892625">"Walang available na emoji"</string>
     <string name="emoji_empty_recent_category" msgid="7863877827879290200">"Hindi ka pa gumamit ng anumang emoji"</string>
+    <!-- no translation found for emoji_bidirectional_switcher_content_desc (5084600168354220605) -->
+    <skip />
 </resources>
diff --git a/emoji2/emoji2-emojipicker/src/main/res/values-tr/strings.xml b/emoji2/emoji2-emojipicker/src/main/res/values-tr/strings.xml
index be6c132..a3f6281 100644
--- a/emoji2/emoji2-emojipicker/src/main/res/values-tr/strings.xml
+++ b/emoji2/emoji2-emojipicker/src/main/res/values-tr/strings.xml
@@ -29,4 +29,6 @@
     <string name="emoji_category_flags" msgid="6185639503532784871">"BAYRAKLAR"</string>
     <string name="emoji_empty_non_recent_category" msgid="288822832574892625">"Kullanılabilir emoji yok"</string>
     <string name="emoji_empty_recent_category" msgid="7863877827879290200">"Henüz emoji kullanmadınız"</string>
+    <!-- no translation found for emoji_bidirectional_switcher_content_desc (5084600168354220605) -->
+    <skip />
 </resources>
diff --git a/emoji2/emoji2-emojipicker/src/main/res/values-uk/strings.xml b/emoji2/emoji2-emojipicker/src/main/res/values-uk/strings.xml
index e346eb9..1f54b13 100644
--- a/emoji2/emoji2-emojipicker/src/main/res/values-uk/strings.xml
+++ b/emoji2/emoji2-emojipicker/src/main/res/values-uk/strings.xml
@@ -29,4 +29,6 @@
     <string name="emoji_category_flags" msgid="6185639503532784871">"ПРАПОРИ"</string>
     <string name="emoji_empty_non_recent_category" msgid="288822832574892625">"Немає смайлів"</string>
     <string name="emoji_empty_recent_category" msgid="7863877827879290200">"Ви ще не використовували смайли"</string>
+    <!-- no translation found for emoji_bidirectional_switcher_content_desc (5084600168354220605) -->
+    <skip />
 </resources>
diff --git a/emoji2/emoji2-emojipicker/src/main/res/values-ur/strings.xml b/emoji2/emoji2-emojipicker/src/main/res/values-ur/strings.xml
index 8bcd99b..207b0c7 100644
--- a/emoji2/emoji2-emojipicker/src/main/res/values-ur/strings.xml
+++ b/emoji2/emoji2-emojipicker/src/main/res/values-ur/strings.xml
@@ -29,4 +29,6 @@
     <string name="emoji_category_flags" msgid="6185639503532784871">"جھنڈے"</string>
     <string name="emoji_empty_non_recent_category" msgid="288822832574892625">"کوئی بھی ایموجی دستیاب نہیں ہے"</string>
     <string name="emoji_empty_recent_category" msgid="7863877827879290200">"آپ نے ابھی تک کوئی بھی ایموجی استعمال نہیں کی ہے"</string>
+    <!-- no translation found for emoji_bidirectional_switcher_content_desc (5084600168354220605) -->
+    <skip />
 </resources>
diff --git a/emoji2/emoji2-emojipicker/src/main/res/values-uz/strings.xml b/emoji2/emoji2-emojipicker/src/main/res/values-uz/strings.xml
index ab3f343..a6de5d3 100644
--- a/emoji2/emoji2-emojipicker/src/main/res/values-uz/strings.xml
+++ b/emoji2/emoji2-emojipicker/src/main/res/values-uz/strings.xml
@@ -29,4 +29,6 @@
     <string name="emoji_category_flags" msgid="6185639503532784871">"BAYROQCHALAR"</string>
     <string name="emoji_empty_non_recent_category" msgid="288822832574892625">"Hech qanday emoji mavjud emas"</string>
     <string name="emoji_empty_recent_category" msgid="7863877827879290200">"Hanuz birorta emoji ishlatmagansiz"</string>
+    <!-- no translation found for emoji_bidirectional_switcher_content_desc (5084600168354220605) -->
+    <skip />
 </resources>
diff --git a/emoji2/emoji2-emojipicker/src/main/res/values-vi/strings.xml b/emoji2/emoji2-emojipicker/src/main/res/values-vi/strings.xml
index 1026037..691cb25 100644
--- a/emoji2/emoji2-emojipicker/src/main/res/values-vi/strings.xml
+++ b/emoji2/emoji2-emojipicker/src/main/res/values-vi/strings.xml
@@ -29,4 +29,6 @@
     <string name="emoji_category_flags" msgid="6185639503532784871">"CỜ"</string>
     <string name="emoji_empty_non_recent_category" msgid="288822832574892625">"Không có biểu tượng cảm xúc nào"</string>
     <string name="emoji_empty_recent_category" msgid="7863877827879290200">"Bạn chưa sử dụng biểu tượng cảm xúc nào"</string>
+    <!-- no translation found for emoji_bidirectional_switcher_content_desc (5084600168354220605) -->
+    <skip />
 </resources>
diff --git a/emoji2/emoji2-emojipicker/src/main/res/values-zh-rCN/strings.xml b/emoji2/emoji2-emojipicker/src/main/res/values-zh-rCN/strings.xml
index 8d6e26e..30c282f 100644
--- a/emoji2/emoji2-emojipicker/src/main/res/values-zh-rCN/strings.xml
+++ b/emoji2/emoji2-emojipicker/src/main/res/values-zh-rCN/strings.xml
@@ -29,4 +29,6 @@
     <string name="emoji_category_flags" msgid="6185639503532784871">"旗帜"</string>
     <string name="emoji_empty_non_recent_category" msgid="288822832574892625">"没有可用的表情符号"</string>
     <string name="emoji_empty_recent_category" msgid="7863877827879290200">"您尚未使用过任何表情符号"</string>
+    <!-- no translation found for emoji_bidirectional_switcher_content_desc (5084600168354220605) -->
+    <skip />
 </resources>
diff --git a/emoji2/emoji2-emojipicker/src/main/res/values-zh-rHK/strings.xml b/emoji2/emoji2-emojipicker/src/main/res/values-zh-rHK/strings.xml
index d2befe8..99faec7 100644
--- a/emoji2/emoji2-emojipicker/src/main/res/values-zh-rHK/strings.xml
+++ b/emoji2/emoji2-emojipicker/src/main/res/values-zh-rHK/strings.xml
@@ -29,4 +29,6 @@
     <string name="emoji_category_flags" msgid="6185639503532784871">"旗幟"</string>
     <string name="emoji_empty_non_recent_category" msgid="288822832574892625">"沒有可用的 Emoji"</string>
     <string name="emoji_empty_recent_category" msgid="7863877827879290200">"你尚未使用任何 Emoji"</string>
+    <!-- no translation found for emoji_bidirectional_switcher_content_desc (5084600168354220605) -->
+    <skip />
 </resources>
diff --git a/emoji2/emoji2-emojipicker/src/main/res/values-zh-rTW/strings.xml b/emoji2/emoji2-emojipicker/src/main/res/values-zh-rTW/strings.xml
index d71ace8..228d97b 100644
--- a/emoji2/emoji2-emojipicker/src/main/res/values-zh-rTW/strings.xml
+++ b/emoji2/emoji2-emojipicker/src/main/res/values-zh-rTW/strings.xml
@@ -29,4 +29,6 @@
     <string name="emoji_category_flags" msgid="6185639503532784871">"旗幟"</string>
     <string name="emoji_empty_non_recent_category" msgid="288822832574892625">"沒有可用的表情符號"</string>
     <string name="emoji_empty_recent_category" msgid="7863877827879290200">"你尚未使用任何表情符號"</string>
+    <!-- no translation found for emoji_bidirectional_switcher_content_desc (5084600168354220605) -->
+    <skip />
 </resources>
diff --git a/emoji2/emoji2-emojipicker/src/main/res/values-zu/strings.xml b/emoji2/emoji2-emojipicker/src/main/res/values-zu/strings.xml
index e2b449e..2ec492c 100644
--- a/emoji2/emoji2-emojipicker/src/main/res/values-zu/strings.xml
+++ b/emoji2/emoji2-emojipicker/src/main/res/values-zu/strings.xml
@@ -29,4 +29,6 @@
     <string name="emoji_category_flags" msgid="6185639503532784871">"AMAFULEGI"</string>
     <string name="emoji_empty_non_recent_category" msgid="288822832574892625">"Awekho ama-emoji atholakalayo"</string>
     <string name="emoji_empty_recent_category" msgid="7863877827879290200">"Awukasebenzisi noma yimaphi ama-emoji okwamanje"</string>
+    <!-- no translation found for emoji_bidirectional_switcher_content_desc (5084600168354220605) -->
+    <skip />
 </resources>
diff --git a/external/libyuv/build.gradle b/external/libyuv/build.gradle
index 6bf1dae..f1b7014 100644
--- a/external/libyuv/build.gradle
+++ b/external/libyuv/build.gradle
@@ -35,7 +35,8 @@
     defaultConfig {
         externalNativeBuild {
             cmake {
-                arguments "-DCMAKE_POLICY_DEFAULT_CMP0064=NEW", "-DCMAKE_VERBOSE_MAKEFILE=ON"
+                arguments "-DCMAKE_POLICY_DEFAULT_CMP0064=NEW",
+                                "-DCMAKE_VERBOSE_MAKEFILE=ON"
                 // Build only the static library target
                 targets "yuv"
             }
diff --git a/fragment/fragment-lint/src/main/java/androidx/fragment/lint/UnsafeRepeatOnLifecycleDetector.kt b/fragment/fragment-lint/src/main/java/androidx/fragment/lint/UnsafeRepeatOnLifecycleDetector.kt
index b852d05..080b10b 100644
--- a/fragment/fragment-lint/src/main/java/androidx/fragment/lint/UnsafeRepeatOnLifecycleDetector.kt
+++ b/fragment/fragment-lint/src/main/java/androidx/fragment/lint/UnsafeRepeatOnLifecycleDetector.kt
@@ -30,7 +30,6 @@
 import org.jetbrains.uast.UClass
 import org.jetbrains.uast.UMethod
 import org.jetbrains.uast.getParentOfType
-import org.jetbrains.uast.kotlin.KotlinUFunctionCallExpression
 
 /**
  * Lint check for detecting calls to the suspend `repeatOnLifecycle` APIs using `lifecycleOwner`
@@ -73,7 +72,7 @@
         if (!isCalledInViewLifecycleFunction(node.getParentOfType())) return
 
         // Look at the entire launch scope
-        var launchScope = node.getParentOfType<KotlinUFunctionCallExpression>()?.receiver
+        var launchScope = node.getParentOfType<UCallExpression>()?.receiver
         while (
             launchScope != null && !containsViewLifecycleOwnerCall(launchScope.sourcePsi?.text)
         ) {
diff --git a/fragment/fragment-lint/src/main/java/androidx/fragment/lint/UseRequireInsteadOfGet.kt b/fragment/fragment-lint/src/main/java/androidx/fragment/lint/UseRequireInsteadOfGet.kt
index 0e8b728c..193833b 100644
--- a/fragment/fragment-lint/src/main/java/androidx/fragment/lint/UseRequireInsteadOfGet.kt
+++ b/fragment/fragment-lint/src/main/java/androidx/fragment/lint/UseRequireInsteadOfGet.kt
@@ -38,7 +38,6 @@
 import org.jetbrains.uast.UQualifiedReferenceExpression
 import org.jetbrains.uast.USimpleNameReferenceExpression
 import org.jetbrains.uast.getContainingUClass
-import org.jetbrains.uast.kotlin.KotlinUFunctionCallExpression
 import org.jetbrains.uast.skipParenthesizedExprDown
 import org.jetbrains.uast.skipParenthesizedExprUp
 import org.jetbrains.uast.toUElement
@@ -218,7 +217,6 @@
             ) = enclosingMethodCall.parameterList.parametersCount == 1 ||
                 (
                     isKotlin &&
-                        nearestNonQualifiedRefParent is KotlinUFunctionCallExpression &&
                         nearestNonQualifiedRefParent.getArgumentForParameter(1) == null
                     )
 
diff --git a/glance/glance-appwidget/src/main/res/values-af/strings.xml b/glance/glance-appwidget/src/main/res/values-af/strings.xml
index 1e3098b..ffbcebb 100644
--- a/glance/glance-appwidget/src/main/res/values-af/strings.xml
+++ b/glance/glance-appwidget/src/main/res/values-af/strings.xml
@@ -19,6 +19,5 @@
     xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
     <string name="glance_error_layout_title" msgid="3631961919234443531"><b>"Glance-applegstukfout"</b></string>
     <string name="glance_error_layout_text" msgid="2863935784364843033">"Gaan die presiese fout na deur "<b><tt>"adb logcat"</tt></b>" te gebruik en te soek na "<b><tt>"GlanceAppWidget"</tt></b></string>
-    <!-- no translation found for glance_error_layout_text_v2 (5191168365305634625) -->
-    <skip />
+    <string name="glance_error_layout_text_v2" msgid="5191168365305634625">"Kan nie inhoud wys nie"</string>
 </resources>
diff --git a/glance/glance-appwidget/src/main/res/values-ar/strings.xml b/glance/glance-appwidget/src/main/res/values-ar/strings.xml
index a4a0d45..7a610e1 100644
--- a/glance/glance-appwidget/src/main/res/values-ar/strings.xml
+++ b/glance/glance-appwidget/src/main/res/values-ar/strings.xml
@@ -19,6 +19,5 @@
     xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
     <string name="glance_error_layout_title" msgid="3631961919234443531">"‏"<b>"خطأ في التطبيق المصغّر Glance"</b></string>
     <string name="glance_error_layout_text" msgid="2863935784364843033">"‏تأكَّد من الخطأ بالضبط باستخدام "<b><tt>"أداة adb logcat"</tt></b>"، من خلال البحث عن "<b><tt>"GlanceAppWidget"</tt></b>"."</string>
-    <!-- no translation found for glance_error_layout_text_v2 (5191168365305634625) -->
-    <skip />
+    <string name="glance_error_layout_text_v2" msgid="5191168365305634625">"يتعذّر عرض المحتوى."</string>
 </resources>
diff --git a/glance/glance-appwidget/src/main/res/values-az/strings.xml b/glance/glance-appwidget/src/main/res/values-az/strings.xml
index 3244dd9..fb38a58 100644
--- a/glance/glance-appwidget/src/main/res/values-az/strings.xml
+++ b/glance/glance-appwidget/src/main/res/values-az/strings.xml
@@ -19,6 +19,5 @@
     xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
     <string name="glance_error_layout_title" msgid="3631961919234443531"><b>"Glance tətbiq vidceti xətası"</b></string>
     <string name="glance_error_layout_text" msgid="2863935784364843033"><b><tt>"Adb logcat"</tt></b>" istifadə edərək, "<b><tt>"GlanceAppWidget"</tt></b>" axtararaq dəqiq xətanı yoxlayın"</string>
-    <!-- no translation found for glance_error_layout_text_v2 (5191168365305634625) -->
-    <skip />
+    <string name="glance_error_layout_text_v2" msgid="5191168365305634625">"Kontenti göstərmək olmur"</string>
 </resources>
diff --git a/glance/glance-appwidget/src/main/res/values-b+sr+Latn/strings.xml b/glance/glance-appwidget/src/main/res/values-b+sr+Latn/strings.xml
index 2194879..9395e03 100644
--- a/glance/glance-appwidget/src/main/res/values-b+sr+Latn/strings.xml
+++ b/glance/glance-appwidget/src/main/res/values-b+sr+Latn/strings.xml
@@ -19,6 +19,5 @@
     xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
     <string name="glance_error_layout_title" msgid="3631961919234443531"><b>"Greška u vezi sa vidžetom aplikacije Glance"</b></string>
     <string name="glance_error_layout_text" msgid="2863935784364843033">"Proverite u čemu je tačno greška pomoću stavke"<b><tt>"adb logcat"</tt></b>", tražeći stavku "<b><tt>"GlanceAppWidget"</tt></b></string>
-    <!-- no translation found for glance_error_layout_text_v2 (5191168365305634625) -->
-    <skip />
+    <string name="glance_error_layout_text_v2" msgid="5191168365305634625">"Sadržaj ne može da se prikaže"</string>
 </resources>
diff --git a/glance/glance-appwidget/src/main/res/values-bg/strings.xml b/glance/glance-appwidget/src/main/res/values-bg/strings.xml
index c71e9d6..996380eb 100644
--- a/glance/glance-appwidget/src/main/res/values-bg/strings.xml
+++ b/glance/glance-appwidget/src/main/res/values-bg/strings.xml
@@ -19,6 +19,5 @@
     xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
     <string name="glance_error_layout_title" msgid="3631961919234443531"><b>"Грешка в приспособлението за приложението Glance"</b></string>
     <string name="glance_error_layout_text" msgid="2863935784364843033">"Проверете точната грешка посредством "<b><tt>"adb logcat"</tt></b>", като потърсите "<b><tt>"GlanceAppWidget"</tt></b></string>
-    <!-- no translation found for glance_error_layout_text_v2 (5191168365305634625) -->
-    <skip />
+    <string name="glance_error_layout_text_v2" msgid="5191168365305634625">"Съдържанието не може да се покаже"</string>
 </resources>
diff --git a/glance/glance-appwidget/src/main/res/values-bs/strings.xml b/glance/glance-appwidget/src/main/res/values-bs/strings.xml
index 0975fca..648882a 100644
--- a/glance/glance-appwidget/src/main/res/values-bs/strings.xml
+++ b/glance/glance-appwidget/src/main/res/values-bs/strings.xml
@@ -19,5 +19,5 @@
     xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
     <string name="glance_error_layout_title" msgid="3631961919234443531"><b>"Greška vidžeta aplikacije Glance"</b></string>
     <string name="glance_error_layout_text" msgid="2863935784364843033">"Provjerite konkretnu grešku koristeći "<b><tt>"adb logcat"</tt></b>" i pretraživanjem "<b><tt>"GlanceAppWidget"</tt></b></string>
-    <string name="glance_error_layout_text_v2" msgid="5191168365305634625">"Sadržaj se ne može prikazati"</string>
+    <string name="glance_error_layout_text_v2" msgid="5191168365305634625">"Nije moguće prikazati sadržaj"</string>
 </resources>
diff --git a/glance/glance-appwidget/src/main/res/values-ca/strings.xml b/glance/glance-appwidget/src/main/res/values-ca/strings.xml
index 5b2cf5c..ce802c4 100644
--- a/glance/glance-appwidget/src/main/res/values-ca/strings.xml
+++ b/glance/glance-appwidget/src/main/res/values-ca/strings.xml
@@ -19,6 +19,5 @@
     xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
     <string name="glance_error_layout_title" msgid="3631961919234443531"><b>"Error de Glance App Widget"</b></string>
     <string name="glance_error_layout_text" msgid="2863935784364843033">"Consulta l\'error exacte fent servir "<b><tt>"adb logcat"</tt></b>" i cercant "<b><tt>"GlanceAppWidget"</tt></b></string>
-    <!-- no translation found for glance_error_layout_text_v2 (5191168365305634625) -->
-    <skip />
+    <string name="glance_error_layout_text_v2" msgid="5191168365305634625">"No es pot mostrar el contingut"</string>
 </resources>
diff --git a/glance/glance-appwidget/src/main/res/values-cs/strings.xml b/glance/glance-appwidget/src/main/res/values-cs/strings.xml
index 872b32b..d0a2742 100644
--- a/glance/glance-appwidget/src/main/res/values-cs/strings.xml
+++ b/glance/glance-appwidget/src/main/res/values-cs/strings.xml
@@ -19,6 +19,5 @@
     xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
     <string name="glance_error_layout_title" msgid="3631961919234443531"><b>"Chyba widgetu aplikace Glance"</b></string>
     <string name="glance_error_layout_text" msgid="2863935784364843033">"Pomocí "<b><tt>"adb logcat"</tt></b>" zkontrolujte přesné znění chyby: vyhledejte "<b><tt>"GlanceAppWidget"</tt></b></string>
-    <!-- no translation found for glance_error_layout_text_v2 (5191168365305634625) -->
-    <skip />
+    <string name="glance_error_layout_text_v2" msgid="5191168365305634625">"Obsah nelze zobrazit"</string>
 </resources>
diff --git a/glance/glance-appwidget/src/main/res/values-da/strings.xml b/glance/glance-appwidget/src/main/res/values-da/strings.xml
index b01ca60..c436f2c 100644
--- a/glance/glance-appwidget/src/main/res/values-da/strings.xml
+++ b/glance/glance-appwidget/src/main/res/values-da/strings.xml
@@ -19,6 +19,5 @@
     xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
     <string name="glance_error_layout_title" msgid="3631961919234443531"><b>"Fejl i appwidgetten Glance"</b></string>
     <string name="glance_error_layout_text" msgid="2863935784364843033">"Brug "<b><tt>"adb logcat"</tt></b>" til at tjekke den specifikke fejl ved at søge efter "<b><tt>"GlanceAppWidget"</tt></b></string>
-    <!-- no translation found for glance_error_layout_text_v2 (5191168365305634625) -->
-    <skip />
+    <string name="glance_error_layout_text_v2" msgid="5191168365305634625">"Indholdet kan ikke vises"</string>
 </resources>
diff --git a/glance/glance-appwidget/src/main/res/values-el/strings.xml b/glance/glance-appwidget/src/main/res/values-el/strings.xml
index b148f7c..3d258e9 100644
--- a/glance/glance-appwidget/src/main/res/values-el/strings.xml
+++ b/glance/glance-appwidget/src/main/res/values-el/strings.xml
@@ -19,6 +19,5 @@
     xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
     <string name="glance_error_layout_title" msgid="3631961919234443531"><b>"Σφάλμα γραφικού στοιχείου εφαρμογής Glance"</b></string>
     <string name="glance_error_layout_text" msgid="2863935784364843033">"Ελέγξτε το ακριβές σφάλμα χρησιμοποιώντας το "<b><tt>"adb logcat"</tt></b>" και αναζητώντας το στοιχείο "<b><tt>"GlanceAppWidget"</tt></b></string>
-    <!-- no translation found for glance_error_layout_text_v2 (5191168365305634625) -->
-    <skip />
+    <string name="glance_error_layout_text_v2" msgid="5191168365305634625">"Δεν είναι δυνατή η εμφάνιση περιεχομένου"</string>
 </resources>
diff --git a/glance/glance-appwidget/src/main/res/values-en-rAU/strings.xml b/glance/glance-appwidget/src/main/res/values-en-rAU/strings.xml
index e812d63..c1251a2 100644
--- a/glance/glance-appwidget/src/main/res/values-en-rAU/strings.xml
+++ b/glance/glance-appwidget/src/main/res/values-en-rAU/strings.xml
@@ -19,6 +19,5 @@
     xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
     <string name="glance_error_layout_title" msgid="3631961919234443531"><b>"Glance app widget error"</b></string>
     <string name="glance_error_layout_text" msgid="2863935784364843033">"Check the exact error using "<b><tt>"adb Logcat"</tt></b>", searching for "<b><tt>"GlanceAppWidget"</tt></b></string>
-    <!-- no translation found for glance_error_layout_text_v2 (5191168365305634625) -->
-    <skip />
+    <string name="glance_error_layout_text_v2" msgid="5191168365305634625">"Can\'t show content"</string>
 </resources>
diff --git a/glance/glance-appwidget/src/main/res/values-en-rGB/strings.xml b/glance/glance-appwidget/src/main/res/values-en-rGB/strings.xml
index e812d63..c1251a2 100644
--- a/glance/glance-appwidget/src/main/res/values-en-rGB/strings.xml
+++ b/glance/glance-appwidget/src/main/res/values-en-rGB/strings.xml
@@ -19,6 +19,5 @@
     xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
     <string name="glance_error_layout_title" msgid="3631961919234443531"><b>"Glance app widget error"</b></string>
     <string name="glance_error_layout_text" msgid="2863935784364843033">"Check the exact error using "<b><tt>"adb Logcat"</tt></b>", searching for "<b><tt>"GlanceAppWidget"</tt></b></string>
-    <!-- no translation found for glance_error_layout_text_v2 (5191168365305634625) -->
-    <skip />
+    <string name="glance_error_layout_text_v2" msgid="5191168365305634625">"Can\'t show content"</string>
 </resources>
diff --git a/glance/glance-appwidget/src/main/res/values-en-rIN/strings.xml b/glance/glance-appwidget/src/main/res/values-en-rIN/strings.xml
index e812d63..c1251a2 100644
--- a/glance/glance-appwidget/src/main/res/values-en-rIN/strings.xml
+++ b/glance/glance-appwidget/src/main/res/values-en-rIN/strings.xml
@@ -19,6 +19,5 @@
     xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
     <string name="glance_error_layout_title" msgid="3631961919234443531"><b>"Glance app widget error"</b></string>
     <string name="glance_error_layout_text" msgid="2863935784364843033">"Check the exact error using "<b><tt>"adb Logcat"</tt></b>", searching for "<b><tt>"GlanceAppWidget"</tt></b></string>
-    <!-- no translation found for glance_error_layout_text_v2 (5191168365305634625) -->
-    <skip />
+    <string name="glance_error_layout_text_v2" msgid="5191168365305634625">"Can\'t show content"</string>
 </resources>
diff --git a/glance/glance-appwidget/src/main/res/values-es/strings.xml b/glance/glance-appwidget/src/main/res/values-es/strings.xml
index 1791bc0..3d45dd1 100644
--- a/glance/glance-appwidget/src/main/res/values-es/strings.xml
+++ b/glance/glance-appwidget/src/main/res/values-es/strings.xml
@@ -19,6 +19,5 @@
     xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
     <string name="glance_error_layout_title" msgid="3631961919234443531"><b>"Error del widget de la aplicación Glance"</b></string>
     <string name="glance_error_layout_text" msgid="2863935784364843033">"Revisa el error exacto mediante "<b><tt>"adb logcat"</tt></b>" y buscando "<b><tt>"GlanceAppWidget"</tt></b></string>
-    <!-- no translation found for glance_error_layout_text_v2 (5191168365305634625) -->
-    <skip />
+    <string name="glance_error_layout_text_v2" msgid="5191168365305634625">"No se puede mostrar el contenido"</string>
 </resources>
diff --git a/glance/glance-appwidget/src/main/res/values-et/strings.xml b/glance/glance-appwidget/src/main/res/values-et/strings.xml
index 3b9ad9a..e1563ad 100644
--- a/glance/glance-appwidget/src/main/res/values-et/strings.xml
+++ b/glance/glance-appwidget/src/main/res/values-et/strings.xml
@@ -19,6 +19,5 @@
     xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
     <string name="glance_error_layout_title" msgid="3631961919234443531"><b>"Rakenduse Ülevaade vidina viga"</b></string>
     <string name="glance_error_layout_text" msgid="2863935784364843033">"Kontrollige konkreetset viga tööriistaga "<b><tt>"adb logcat"</tt></b>", sisestades otsingu "<b><tt>"GlanceAppWidget"</tt></b></string>
-    <!-- no translation found for glance_error_layout_text_v2 (5191168365305634625) -->
-    <skip />
+    <string name="glance_error_layout_text_v2" msgid="5191168365305634625">"Sisu ei saa kuvada"</string>
 </resources>
diff --git a/glance/glance-appwidget/src/main/res/values-eu/strings.xml b/glance/glance-appwidget/src/main/res/values-eu/strings.xml
index e67b27f..40ab942 100644
--- a/glance/glance-appwidget/src/main/res/values-eu/strings.xml
+++ b/glance/glance-appwidget/src/main/res/values-eu/strings.xml
@@ -19,6 +19,5 @@
     xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
     <string name="glance_error_layout_title" msgid="3631961919234443531"><b>"Glance aplikazioaren widgetaren errorea"</b></string>
     <string name="glance_error_layout_text" msgid="2863935784364843033">"Errore zehatza ikusteko, erabili "<b><tt>"adb logcat"</tt></b>" bertan "<b><tt>"GlanceAppWidget"</tt></b>" bilatzeko"</string>
-    <!-- no translation found for glance_error_layout_text_v2 (5191168365305634625) -->
-    <skip />
+    <string name="glance_error_layout_text_v2" msgid="5191168365305634625">"Ezin da erakutsi edukia"</string>
 </resources>
diff --git a/glance/glance-appwidget/src/main/res/values-fr-rCA/strings.xml b/glance/glance-appwidget/src/main/res/values-fr-rCA/strings.xml
index 7b97e4f..e948735 100644
--- a/glance/glance-appwidget/src/main/res/values-fr-rCA/strings.xml
+++ b/glance/glance-appwidget/src/main/res/values-fr-rCA/strings.xml
@@ -19,6 +19,5 @@
     xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
     <string name="glance_error_layout_title" msgid="3631961919234443531"><b>"Erreur provenant de la librairie Glance App Widget"</b></string>
     <string name="glance_error_layout_text" msgid="2863935784364843033">"Vérifiez l\'erreur exacte en utilisant "<b><tt>"adb logcat"</tt></b>", en recherchant "<b><tt>"GlanceAppWidget"</tt></b></string>
-    <!-- no translation found for glance_error_layout_text_v2 (5191168365305634625) -->
-    <skip />
+    <string name="glance_error_layout_text_v2" msgid="5191168365305634625">"Impossible d\'afficher le contenu"</string>
 </resources>
diff --git a/glance/glance-appwidget/src/main/res/values-gl/strings.xml b/glance/glance-appwidget/src/main/res/values-gl/strings.xml
index c223cba..d255bb4 100644
--- a/glance/glance-appwidget/src/main/res/values-gl/strings.xml
+++ b/glance/glance-appwidget/src/main/res/values-gl/strings.xml
@@ -19,6 +19,5 @@
     xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
     <string name="glance_error_layout_title" msgid="3631961919234443531"><b>"Erro do widget da aplicación Glance"</b></string>
     <string name="glance_error_layout_text" msgid="2863935784364843033">"Comproba o erro exacto usando "<b><tt>"adb logcat"</tt></b>" e buscando "<b><tt>"GlanceAppWidget"</tt></b></string>
-    <!-- no translation found for glance_error_layout_text_v2 (5191168365305634625) -->
-    <skip />
+    <string name="glance_error_layout_text_v2" msgid="5191168365305634625">"Non se puido mostrar o contido"</string>
 </resources>
diff --git a/glance/glance-appwidget/src/main/res/values-gu/strings.xml b/glance/glance-appwidget/src/main/res/values-gu/strings.xml
index a378b09..c1f6e06 100644
--- a/glance/glance-appwidget/src/main/res/values-gu/strings.xml
+++ b/glance/glance-appwidget/src/main/res/values-gu/strings.xml
@@ -19,6 +19,5 @@
     xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
     <string name="glance_error_layout_title" msgid="3631961919234443531"><b>"Glance ઍપ વિજેટમાં ભૂલ"</b></string>
     <string name="glance_error_layout_text" msgid="2863935784364843033"><b><tt>"adb logcat"</tt></b>"નો ઉપયોગ કરીને, "<b><tt>"GlanceAppWidget"</tt></b>" શોધીને ચોક્કસ ભૂલ ચેક કરો"</string>
-    <!-- no translation found for glance_error_layout_text_v2 (5191168365305634625) -->
-    <skip />
+    <string name="glance_error_layout_text_v2" msgid="5191168365305634625">"કન્ટેન્ટ બતાવી શકતા નથી"</string>
 </resources>
diff --git a/glance/glance-appwidget/src/main/res/values-hi/strings.xml b/glance/glance-appwidget/src/main/res/values-hi/strings.xml
index 574e22b..a41f5fa 100644
--- a/glance/glance-appwidget/src/main/res/values-hi/strings.xml
+++ b/glance/glance-appwidget/src/main/res/values-hi/strings.xml
@@ -19,6 +19,5 @@
     xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
     <string name="glance_error_layout_title" msgid="3631961919234443531"><b>"Glance App Widget की गड़बड़ी"</b></string>
     <string name="glance_error_layout_text" msgid="2863935784364843033"><b><tt>"adb logcat"</tt></b>" की मदद से, "<b><tt>"GlanceAppWidget"</tt></b>" को ढूंढकर असल गड़बड़ी का पता लगाएं"</string>
-    <!-- no translation found for glance_error_layout_text_v2 (5191168365305634625) -->
-    <skip />
+    <string name="glance_error_layout_text_v2" msgid="5191168365305634625">"कॉन्टेंट नहीं दिखाया जा सकता"</string>
 </resources>
diff --git a/glance/glance-appwidget/src/main/res/values-hy/strings.xml b/glance/glance-appwidget/src/main/res/values-hy/strings.xml
index 3409cd3..f02aceb 100644
--- a/glance/glance-appwidget/src/main/res/values-hy/strings.xml
+++ b/glance/glance-appwidget/src/main/res/values-hy/strings.xml
@@ -19,6 +19,5 @@
     xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
     <string name="glance_error_layout_title" msgid="3631961919234443531"><b>"«Հակիրճ գլխավորի մասին» վիջեթի սխալ"</b></string>
     <string name="glance_error_layout_text" msgid="2863935784364843033">"Ստուգեք, թե իրականում ինչ սխալ է տեղի ունեցել, "<b><tt>"adb logcat"</tt></b>"-ի միջոցով՝ որոնելով "<b><tt>"GlanceAppWidget"</tt></b>" բառը"</string>
-    <!-- no translation found for glance_error_layout_text_v2 (5191168365305634625) -->
-    <skip />
+    <string name="glance_error_layout_text_v2" msgid="5191168365305634625">"Հնարավոր չէ ցույց տալ բովանդակությունը"</string>
 </resources>
diff --git a/glance/glance-appwidget/src/main/res/values-in/strings.xml b/glance/glance-appwidget/src/main/res/values-in/strings.xml
index 995c8f9..2279d9f 100644
--- a/glance/glance-appwidget/src/main/res/values-in/strings.xml
+++ b/glance/glance-appwidget/src/main/res/values-in/strings.xml
@@ -19,6 +19,5 @@
     xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
     <string name="glance_error_layout_title" msgid="3631961919234443531"><b>"Error library Widget Aplikasi Glance"</b></string>
     <string name="glance_error_layout_text" msgid="2863935784364843033">"Periksa error yang tepat menggunakan "<b><tt>"logcat adb"</tt></b>" saat menelusuri "<b><tt>"GlanceAppWidget"</tt></b></string>
-    <!-- no translation found for glance_error_layout_text_v2 (5191168365305634625) -->
-    <skip />
+    <string name="glance_error_layout_text_v2" msgid="5191168365305634625">"Tidak dapat menampilkan konten"</string>
 </resources>
diff --git a/glance/glance-appwidget/src/main/res/values-is/strings.xml b/glance/glance-appwidget/src/main/res/values-is/strings.xml
index af6d0fb..7dd56b0 100644
--- a/glance/glance-appwidget/src/main/res/values-is/strings.xml
+++ b/glance/glance-appwidget/src/main/res/values-is/strings.xml
@@ -19,6 +19,5 @@
     xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
     <string name="glance_error_layout_title" msgid="3631961919234443531"><b>"Villa í forritsgræju „Í fljótu bragði“"</b></string>
     <string name="glance_error_layout_text" msgid="2863935784364843033">"Skoðaðu villuna með "<b><tt>"adb logcat"</tt></b>" og leitaðu að "<b><tt>"GlanceAppWidget"</tt></b></string>
-    <!-- no translation found for glance_error_layout_text_v2 (5191168365305634625) -->
-    <skip />
+    <string name="glance_error_layout_text_v2" msgid="5191168365305634625">"Ekki er hægt að sýna efnið"</string>
 </resources>
diff --git a/glance/glance-appwidget/src/main/res/values-it/strings.xml b/glance/glance-appwidget/src/main/res/values-it/strings.xml
index a169b36..392f1eb 100644
--- a/glance/glance-appwidget/src/main/res/values-it/strings.xml
+++ b/glance/glance-appwidget/src/main/res/values-it/strings.xml
@@ -19,6 +19,5 @@
     xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
     <string name="glance_error_layout_title" msgid="3631961919234443531"><b>"Errore del widget dell\'app Glance"</b></string>
     <string name="glance_error_layout_text" msgid="2863935784364843033">"Controlla l\'errore esatto con "<b><tt>"adb logcat"</tt></b>", cercando "<b><tt>"GlanceAppWidget"</tt></b></string>
-    <!-- no translation found for glance_error_layout_text_v2 (5191168365305634625) -->
-    <skip />
+    <string name="glance_error_layout_text_v2" msgid="5191168365305634625">"Impossibile mostrare i contenuti"</string>
 </resources>
diff --git a/glance/glance-appwidget/src/main/res/values-iw/strings.xml b/glance/glance-appwidget/src/main/res/values-iw/strings.xml
index cbfc3b8..f1525fd 100644
--- a/glance/glance-appwidget/src/main/res/values-iw/strings.xml
+++ b/glance/glance-appwidget/src/main/res/values-iw/strings.xml
@@ -19,6 +19,5 @@
     xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
     <string name="glance_error_layout_title" msgid="3631961919234443531"><b>"שגיאה בווידג\'ט של האפליקציה \'בקצרה\'"</b></string>
     <string name="glance_error_layout_text" msgid="2863935784364843033">"‏יש לבדוק את השגיאה המדויקת באמצעות "<b><tt>"adb Logcat‏"</tt></b>", ולחפש את "<b><tt>"GlanceAppWidget"</tt></b></string>
-    <!-- no translation found for glance_error_layout_text_v2 (5191168365305634625) -->
-    <skip />
+    <string name="glance_error_layout_text_v2" msgid="5191168365305634625">"לא ניתן להציג את התוכן"</string>
 </resources>
diff --git a/glance/glance-appwidget/src/main/res/values-kk/strings.xml b/glance/glance-appwidget/src/main/res/values-kk/strings.xml
index 8268784..6737835 100644
--- a/glance/glance-appwidget/src/main/res/values-kk/strings.xml
+++ b/glance/glance-appwidget/src/main/res/values-kk/strings.xml
@@ -19,6 +19,5 @@
     xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
     <string name="glance_error_layout_title" msgid="3631961919234443531"><b>"Glance App Widget қатесі"</b></string>
     <string name="glance_error_layout_text" msgid="2863935784364843033">"Қатені дәл анықтау үшін "<b><tt>"adb logcat"</tt></b>" мәнін қолданыңыз және "<b><tt>"GlanceAppWidget"</tt></b>" іздеңіз."</string>
-    <!-- no translation found for glance_error_layout_text_v2 (5191168365305634625) -->
-    <skip />
+    <string name="glance_error_layout_text_v2" msgid="5191168365305634625">"Контентті көрсету мүмкін емес."</string>
 </resources>
diff --git a/glance/glance-appwidget/src/main/res/values-kn/strings.xml b/glance/glance-appwidget/src/main/res/values-kn/strings.xml
index f02fd27..caee2e0 100644
--- a/glance/glance-appwidget/src/main/res/values-kn/strings.xml
+++ b/glance/glance-appwidget/src/main/res/values-kn/strings.xml
@@ -19,6 +19,5 @@
     xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
     <string name="glance_error_layout_title" msgid="3631961919234443531"><b>"Glance ಆ್ಯಪ್ ವಿಜೆಟ್ ದೋಷ"</b></string>
     <string name="glance_error_layout_text" msgid="2863935784364843033"><b><tt>"adb logcat"</tt></b>" ಬಳಸಿಕೊಂಡು ನಿಖರವಾದ ದೋಷವನ್ನು ಪರಿಶೀಲಿಸಿ, "<b><tt>"GlanceAppWidget"</tt></b>" ಅನ್ನು ಹುಡುಕಲಾಗುತ್ತಿದೆ"</string>
-    <!-- no translation found for glance_error_layout_text_v2 (5191168365305634625) -->
-    <skip />
+    <string name="glance_error_layout_text_v2" msgid="5191168365305634625">"ಕಂಟೆಂಟ್ ಅನ್ನು ತೋರಿಸಲು ಸಾಧ್ಯವಿಲ್ಲ"</string>
 </resources>
diff --git a/glance/glance-appwidget/src/main/res/values-ko/strings.xml b/glance/glance-appwidget/src/main/res/values-ko/strings.xml
index 527abf1..5decae6 100644
--- a/glance/glance-appwidget/src/main/res/values-ko/strings.xml
+++ b/glance/glance-appwidget/src/main/res/values-ko/strings.xml
@@ -19,6 +19,5 @@
     xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
     <string name="glance_error_layout_title" msgid="3631961919234443531"><b>"Glance 앱 위젯 오류"</b></string>
     <string name="glance_error_layout_text" msgid="2863935784364843033"><b><tt>"adb logcat"</tt></b>"을 사용하여 정확한 오류 확인, "<b><tt>"GlanceAppWidget"</tt></b>" 검색"</string>
-    <!-- no translation found for glance_error_layout_text_v2 (5191168365305634625) -->
-    <skip />
+    <string name="glance_error_layout_text_v2" msgid="5191168365305634625">"콘텐츠를 표시할 수 없음"</string>
 </resources>
diff --git a/glance/glance-appwidget/src/main/res/values-ky/strings.xml b/glance/glance-appwidget/src/main/res/values-ky/strings.xml
index 8946e1c..78ad931 100644
--- a/glance/glance-appwidget/src/main/res/values-ky/strings.xml
+++ b/glance/glance-appwidget/src/main/res/values-ky/strings.xml
@@ -19,6 +19,5 @@
     xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
     <string name="glance_error_layout_title" msgid="3631961919234443531"><b>"Glance колдонмосунун виджет катасы"</b></string>
     <string name="glance_error_layout_text" msgid="2863935784364843033"><b><tt>"GlanceAppWidget"</tt></b>" cурамы менен издеп, "<b><tt>"adb logcat"</tt></b>" аркылуу катаны аныктаңыз"</string>
-    <!-- no translation found for glance_error_layout_text_v2 (5191168365305634625) -->
-    <skip />
+    <string name="glance_error_layout_text_v2" msgid="5191168365305634625">"Контент көрсөтүлбөй жатат"</string>
 </resources>
diff --git a/glance/glance-appwidget/src/main/res/values-lt/strings.xml b/glance/glance-appwidget/src/main/res/values-lt/strings.xml
index d2032a8..6576dc2 100644
--- a/glance/glance-appwidget/src/main/res/values-lt/strings.xml
+++ b/glance/glance-appwidget/src/main/res/values-lt/strings.xml
@@ -19,6 +19,5 @@
     xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
     <string name="glance_error_layout_title" msgid="3631961919234443531"><b>"Programos „Glance“ valdiklio klaida"</b></string>
     <string name="glance_error_layout_text" msgid="2863935784364843033">"Patikrinkite tikslią klaidą naudodami "<b><tt>"ADB Logcat"</tt></b>", kai ieškote "<b><tt>"GlanceAppWidget"</tt></b></string>
-    <!-- no translation found for glance_error_layout_text_v2 (5191168365305634625) -->
-    <skip />
+    <string name="glance_error_layout_text_v2" msgid="5191168365305634625">"Nepavyko parodyti turinio"</string>
 </resources>
diff --git a/glance/glance-appwidget/src/main/res/values-lv/strings.xml b/glance/glance-appwidget/src/main/res/values-lv/strings.xml
index a132297..eca03a1 100644
--- a/glance/glance-appwidget/src/main/res/values-lv/strings.xml
+++ b/glance/glance-appwidget/src/main/res/values-lv/strings.xml
@@ -19,6 +19,5 @@
     xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
     <string name="glance_error_layout_title" msgid="3631961919234443531"><b>"Glance lietotnes logrīka kļūda"</b></string>
     <string name="glance_error_layout_text" msgid="2863935784364843033">"Atrodiet precīzu kļūdu ar rīka "<b><tt>"adb logcat"</tt></b>" palīdzību, izmantojot meklēšanas vaicājumu "<b><tt>"GlanceAppWidget"</tt></b>"."</string>
-    <!-- no translation found for glance_error_layout_text_v2 (5191168365305634625) -->
-    <skip />
+    <string name="glance_error_layout_text_v2" msgid="5191168365305634625">"Nevar parādīt saturu."</string>
 </resources>
diff --git a/glance/glance-appwidget/src/main/res/values-mk/strings.xml b/glance/glance-appwidget/src/main/res/values-mk/strings.xml
index da90e36..b514004 100644
--- a/glance/glance-appwidget/src/main/res/values-mk/strings.xml
+++ b/glance/glance-appwidget/src/main/res/values-mk/strings.xml
@@ -19,6 +19,5 @@
     xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
     <string name="glance_error_layout_title" msgid="3631961919234443531"><b>"Грешка на Glance App Widget"</b></string>
     <string name="glance_error_layout_text" msgid="2863935784364843033">"Проверете ја точната грешка со "<b><tt>"adb logcat"</tt></b>" при пребарување "<b><tt>"GlanceAppWidget"</tt></b></string>
-    <!-- no translation found for glance_error_layout_text_v2 (5191168365305634625) -->
-    <skip />
+    <string name="glance_error_layout_text_v2" msgid="5191168365305634625">"Не може да се прикажат содржините"</string>
 </resources>
diff --git a/glance/glance-appwidget/src/main/res/values-mn/strings.xml b/glance/glance-appwidget/src/main/res/values-mn/strings.xml
index 8e40cac..48444c1 100644
--- a/glance/glance-appwidget/src/main/res/values-mn/strings.xml
+++ b/glance/glance-appwidget/src/main/res/values-mn/strings.xml
@@ -19,6 +19,5 @@
     xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
     <string name="glance_error_layout_title" msgid="3631961919234443531"><b>"Гялс харах аппын виджетийн алдаа"</b></string>
     <string name="glance_error_layout_text" msgid="2863935784364843033"><b><tt>"Adb logcat"</tt></b>" ашиглан, "<b><tt>"GlanceAppWidget"</tt></b>"-г хайж тодорхой алдааг шалгана уу"</string>
-    <!-- no translation found for glance_error_layout_text_v2 (5191168365305634625) -->
-    <skip />
+    <string name="glance_error_layout_text_v2" msgid="5191168365305634625">"Контентыг харуулах боломжгүй"</string>
 </resources>
diff --git a/glance/glance-appwidget/src/main/res/values-mr/strings.xml b/glance/glance-appwidget/src/main/res/values-mr/strings.xml
index 9334dde..523bc89 100644
--- a/glance/glance-appwidget/src/main/res/values-mr/strings.xml
+++ b/glance/glance-appwidget/src/main/res/values-mr/strings.xml
@@ -19,6 +19,5 @@
     xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
     <string name="glance_error_layout_title" msgid="3631961919234443531"><b>"Glance अ‍ॅप विजेट एरर"</b></string>
     <string name="glance_error_layout_text" msgid="2863935784364843033"><b><tt>"GlanceAppWidget"</tt></b>" शोधून, "<b><tt>"adb logcat"</tt></b>" वापरून नेमकी एरर तपासा"</string>
-    <!-- no translation found for glance_error_layout_text_v2 (5191168365305634625) -->
-    <skip />
+    <string name="glance_error_layout_text_v2" msgid="5191168365305634625">"आशय दाखवू शकत नाही"</string>
 </resources>
diff --git a/glance/glance-appwidget/src/main/res/values-my/strings.xml b/glance/glance-appwidget/src/main/res/values-my/strings.xml
index 662b0b3..db2dcbe 100644
--- a/glance/glance-appwidget/src/main/res/values-my/strings.xml
+++ b/glance/glance-appwidget/src/main/res/values-my/strings.xml
@@ -19,6 +19,5 @@
     xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
     <string name="glance_error_layout_title" msgid="3631961919234443531"><b>"Glance အက်ပ်ဝိဂျက်အမှား"</b></string>
     <string name="glance_error_layout_text" msgid="2863935784364843033"><b><tt>"GlanceAppWidget"</tt></b>" ရှာဖွေရာတွင် "<b><tt>"adb logcat"</tt></b>" သုံးပြီး အမှားအတိအကျကို ရှာနိုင်သည်"</string>
-    <!-- no translation found for glance_error_layout_text_v2 (5191168365305634625) -->
-    <skip />
+    <string name="glance_error_layout_text_v2" msgid="5191168365305634625">"အကြောင်းအရာကို ပြ၍မရပါ"</string>
 </resources>
diff --git a/glance/glance-appwidget/src/main/res/values-nb/strings.xml b/glance/glance-appwidget/src/main/res/values-nb/strings.xml
index 8a31582..d71569f 100644
--- a/glance/glance-appwidget/src/main/res/values-nb/strings.xml
+++ b/glance/glance-appwidget/src/main/res/values-nb/strings.xml
@@ -19,6 +19,5 @@
     xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
     <string name="glance_error_layout_title" msgid="3631961919234443531"><b>"Modulfeil med Kort fortalt-appen"</b></string>
     <string name="glance_error_layout_text" msgid="2863935784364843033">"Sjekk den nøyaktige feilen ved å bruke "<b><tt>"adb logcat"</tt></b>" og søke etter "<b><tt>"GlanceAppWidget"</tt></b></string>
-    <!-- no translation found for glance_error_layout_text_v2 (5191168365305634625) -->
-    <skip />
+    <string name="glance_error_layout_text_v2" msgid="5191168365305634625">"Kan ikke vise innhold"</string>
 </resources>
diff --git a/glance/glance-appwidget/src/main/res/values-ne/strings.xml b/glance/glance-appwidget/src/main/res/values-ne/strings.xml
index bf4dc05..c945179 100644
--- a/glance/glance-appwidget/src/main/res/values-ne/strings.xml
+++ b/glance/glance-appwidget/src/main/res/values-ne/strings.xml
@@ -19,6 +19,5 @@
     xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
     <string name="glance_error_layout_title" msgid="3631961919234443531"><b>"Glance एपको विजेटसम्बन्धी त्रुटि"</b></string>
     <string name="glance_error_layout_text" msgid="2863935784364843033"><b><tt>"adb logcat"</tt></b>" प्रयोग गरी "<b><tt>"GlanceAppWidget"</tt></b>" खोजेर यथार्थ त्रुटि पत्ता लगाउनुहोस्"</string>
-    <!-- no translation found for glance_error_layout_text_v2 (5191168365305634625) -->
-    <skip />
+    <string name="glance_error_layout_text_v2" msgid="5191168365305634625">"सामग्री देखाउन सकिएन"</string>
 </resources>
diff --git a/glance/glance-appwidget/src/main/res/values-or/strings.xml b/glance/glance-appwidget/src/main/res/values-or/strings.xml
index 82a8829..96b460a 100644
--- a/glance/glance-appwidget/src/main/res/values-or/strings.xml
+++ b/glance/glance-appwidget/src/main/res/values-or/strings.xml
@@ -19,6 +19,5 @@
     xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
     <string name="glance_error_layout_title" msgid="3631961919234443531"><b>"Glance ଆପ ୱିଜେଟରେ ତ୍ରୁଟି"</b></string>
     <string name="glance_error_layout_text" msgid="2863935784364843033"><b><tt>"adb logcat"</tt></b>" ବ୍ୟବହାର କରି, "<b><tt>"GlanceAppWidget"</tt></b>" ସନ୍ଧାନ କରି ପ୍ରକୃତ ତ୍ରୁଟି ଯାଞ୍ଚ କରନ୍ତୁ"</string>
-    <!-- no translation found for glance_error_layout_text_v2 (5191168365305634625) -->
-    <skip />
+    <string name="glance_error_layout_text_v2" msgid="5191168365305634625">"ବିଷୟବସ୍ତୁ ଦେଖାଯାଇପାରିବ ନାହିଁ"</string>
 </resources>
diff --git a/glance/glance-appwidget/src/main/res/values-pa/strings.xml b/glance/glance-appwidget/src/main/res/values-pa/strings.xml
index f2d0ab9..15bf9ca 100644
--- a/glance/glance-appwidget/src/main/res/values-pa/strings.xml
+++ b/glance/glance-appwidget/src/main/res/values-pa/strings.xml
@@ -19,6 +19,5 @@
     xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
     <string name="glance_error_layout_title" msgid="3631961919234443531"><b>"Glance ਐਪ ਵਿਜੇਟ ਸੰਬੰਧੀ ਗੜਬੜ"</b></string>
     <string name="glance_error_layout_text" msgid="2863935784364843033"><b><tt>"adb logcat"</tt>" ਦੀ ਵਰਤੋਂ ਨਾਲ "</b>", "<b><tt>"GlanceAppWidget"</tt></b>" ਦੀ ਖੋਜ ਕਰ ਕੇ ਸਟੀਕ ਗੜਬੜ ਦੀ ਜਾਂਚ ਕਰੋ"</string>
-    <!-- no translation found for glance_error_layout_text_v2 (5191168365305634625) -->
-    <skip />
+    <string name="glance_error_layout_text_v2" msgid="5191168365305634625">"ਸਮੱਗਰੀ ਨਹੀਂ ਦਿਖਾਈ ਜਾ ਸਕਦੀ"</string>
 </resources>
diff --git a/glance/glance-appwidget/src/main/res/values-pl/strings.xml b/glance/glance-appwidget/src/main/res/values-pl/strings.xml
index 7482a96..1c58f02 100644
--- a/glance/glance-appwidget/src/main/res/values-pl/strings.xml
+++ b/glance/glance-appwidget/src/main/res/values-pl/strings.xml
@@ -19,6 +19,5 @@
     xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
     <string name="glance_error_layout_title" msgid="3631961919234443531"><b>"Błąd widżetu aplikacji W skrócie"</b></string>
     <string name="glance_error_layout_text" msgid="2863935784364843033">"Sprawdź dokładny błąd w "<b><tt>"adb logcat"</tt></b>" (wyszukaj "<b><tt>"GlanceAppWidget"</tt></b>")"</string>
-    <!-- no translation found for glance_error_layout_text_v2 (5191168365305634625) -->
-    <skip />
+    <string name="glance_error_layout_text_v2" msgid="5191168365305634625">"Nie można wyświetlić zawartości"</string>
 </resources>
diff --git a/glance/glance-appwidget/src/main/res/values-ro/strings.xml b/glance/glance-appwidget/src/main/res/values-ro/strings.xml
index a7e1041..8a0751f 100644
--- a/glance/glance-appwidget/src/main/res/values-ro/strings.xml
+++ b/glance/glance-appwidget/src/main/res/values-ro/strings.xml
@@ -19,6 +19,5 @@
     xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
     <string name="glance_error_layout_title" msgid="3631961919234443531"><b>"Eroare legată de widgetul aplicației Glance"</b></string>
     <string name="glance_error_layout_text" msgid="2863935784364843033">"Vezi eroarea exactă folosind "<b><tt>"adb logcat"</tt></b>" și căutând "<b><tt>"GlanceAppWidget"</tt></b></string>
-    <!-- no translation found for glance_error_layout_text_v2 (5191168365305634625) -->
-    <skip />
+    <string name="glance_error_layout_text_v2" msgid="5191168365305634625">"Nu se poate afișa conținutul"</string>
 </resources>
diff --git a/glance/glance-appwidget/src/main/res/values-ru/strings.xml b/glance/glance-appwidget/src/main/res/values-ru/strings.xml
index 29efc59..109ae4b 100644
--- a/glance/glance-appwidget/src/main/res/values-ru/strings.xml
+++ b/glance/glance-appwidget/src/main/res/values-ru/strings.xml
@@ -19,6 +19,5 @@
     xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
     <string name="glance_error_layout_title" msgid="3631961919234443531"><b>"Ошибка, связанная с виджетом приложения в Glance"</b></string>
     <string name="glance_error_layout_text" msgid="2863935784364843033">"Выявите ошибку с помощью "<b><tt>"adb logcat"</tt></b>". В этом инструменте выполните поиск по запросу "<b><tt>"GlanceAppWidget"</tt></b>"."</string>
-    <!-- no translation found for glance_error_layout_text_v2 (5191168365305634625) -->
-    <skip />
+    <string name="glance_error_layout_text_v2" msgid="5191168365305634625">"Невозможно показать контент."</string>
 </resources>
diff --git a/glance/glance-appwidget/src/main/res/values-si/strings.xml b/glance/glance-appwidget/src/main/res/values-si/strings.xml
index 31bb754..5a2e4b1 100644
--- a/glance/glance-appwidget/src/main/res/values-si/strings.xml
+++ b/glance/glance-appwidget/src/main/res/values-si/strings.xml
@@ -19,6 +19,5 @@
     xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
     <string name="glance_error_layout_title" msgid="3631961919234443531"><b>"Glance යෙදුම් විජට්ටු දෝෂය"</b></string>
     <string name="glance_error_layout_text" msgid="2863935784364843033">"නිවැරදි දෝෂය "<b><tt>"GlanceAppWidget"</tt></b>" සොයන "<b><tt>"adb logcat"</tt></b>" භාවිත කර පරීක්ෂා කරන්න"</string>
-    <!-- no translation found for glance_error_layout_text_v2 (5191168365305634625) -->
-    <skip />
+    <string name="glance_error_layout_text_v2" msgid="5191168365305634625">"අන්තර්ගතය පෙන්විය නොහැක"</string>
 </resources>
diff --git a/glance/glance-appwidget/src/main/res/values-sq/strings.xml b/glance/glance-appwidget/src/main/res/values-sq/strings.xml
index 3d04098..2322305 100644
--- a/glance/glance-appwidget/src/main/res/values-sq/strings.xml
+++ b/glance/glance-appwidget/src/main/res/values-sq/strings.xml
@@ -19,6 +19,5 @@
     xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
     <string name="glance_error_layout_title" msgid="3631961919234443531"><b>"Gabim i Glance App Widget"</b></string>
     <string name="glance_error_layout_text" msgid="2863935784364843033">"Kontrollo gabimin ekzakt duke përdorur "<b><tt>"adb logcat"</tt></b>", duke kërkuar për "<b><tt>"GlanceAppWidget"</tt></b></string>
-    <!-- no translation found for glance_error_layout_text_v2 (5191168365305634625) -->
-    <skip />
+    <string name="glance_error_layout_text_v2" msgid="5191168365305634625">"Përmbajtja nuk mund të shfaqet"</string>
 </resources>
diff --git a/glance/glance-appwidget/src/main/res/values-sr/strings.xml b/glance/glance-appwidget/src/main/res/values-sr/strings.xml
index 3fbcc79..96e2038 100644
--- a/glance/glance-appwidget/src/main/res/values-sr/strings.xml
+++ b/glance/glance-appwidget/src/main/res/values-sr/strings.xml
@@ -19,6 +19,5 @@
     xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
     <string name="glance_error_layout_title" msgid="3631961919234443531"><b>"Грешка у вези са виџетом апликације Glance"</b></string>
     <string name="glance_error_layout_text" msgid="2863935784364843033">"Проверите у чему је тачно грешка помоћу ставке"<b><tt>"adb logcat"</tt></b>", тражећи ставку "<b><tt>"GlanceAppWidget"</tt></b></string>
-    <!-- no translation found for glance_error_layout_text_v2 (5191168365305634625) -->
-    <skip />
+    <string name="glance_error_layout_text_v2" msgid="5191168365305634625">"Садржај не може да се прикаже"</string>
 </resources>
diff --git a/glance/glance-appwidget/src/main/res/values-sv/strings.xml b/glance/glance-appwidget/src/main/res/values-sv/strings.xml
index efcc2fb..840f5c8 100644
--- a/glance/glance-appwidget/src/main/res/values-sv/strings.xml
+++ b/glance/glance-appwidget/src/main/res/values-sv/strings.xml
@@ -19,6 +19,5 @@
     xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
     <string name="glance_error_layout_title" msgid="3631961919234443531"><b>"Fel i appwidget som skapats med Glance"</b></string>
     <string name="glance_error_layout_text" msgid="2863935784364843033">"Kontrollera exakt vad som är fel genom att söka efter "<b><tt>"GlanceAppWidget"</tt></b>" i utdata från "<b><tt>"adb logcat"</tt></b>"."</string>
-    <!-- no translation found for glance_error_layout_text_v2 (5191168365305634625) -->
-    <skip />
+    <string name="glance_error_layout_text_v2" msgid="5191168365305634625">"Det gick inte att visa innehållet"</string>
 </resources>
diff --git a/glance/glance-appwidget/src/main/res/values-ta/strings.xml b/glance/glance-appwidget/src/main/res/values-ta/strings.xml
index 35e91f5..bc993ea 100644
--- a/glance/glance-appwidget/src/main/res/values-ta/strings.xml
+++ b/glance/glance-appwidget/src/main/res/values-ta/strings.xml
@@ -19,6 +19,5 @@
     xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
     <string name="glance_error_layout_title" msgid="3631961919234443531"><b>"Glance ஆப்ஸ் விட்ஜெட் பிழை"</b></string>
     <string name="glance_error_layout_text" msgid="2863935784364843033"><b><tt>"adb logcat"</tt></b>" என்பதைப் பயன்படுத்தி சரியான பிழையைக் கண்டறியவும், "<b><tt>"GlanceAppWidget"</tt></b>" தேடப்படுகிறது"</string>
-    <!-- no translation found for glance_error_layout_text_v2 (5191168365305634625) -->
-    <skip />
+    <string name="glance_error_layout_text_v2" msgid="5191168365305634625">"உள்ளடக்கத்தைக் காட்ட முடியவில்லை"</string>
 </resources>
diff --git a/glance/glance-appwidget/src/main/res/values-tl/strings.xml b/glance/glance-appwidget/src/main/res/values-tl/strings.xml
index 95d2331..5a80654 100644
--- a/glance/glance-appwidget/src/main/res/values-tl/strings.xml
+++ b/glance/glance-appwidget/src/main/res/values-tl/strings.xml
@@ -19,6 +19,5 @@
     xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
     <string name="glance_error_layout_title" msgid="3631961919234443531"><b>"Error sa Widget ng Glance App"</b></string>
     <string name="glance_error_layout_text" msgid="2863935784364843033">"Tingnan ang eksaktong error gamit ang "<b><tt>"adb logcat"</tt></b>", at paghahanap sa "<b><tt>"GlanceAppWidget"</tt></b></string>
-    <!-- no translation found for glance_error_layout_text_v2 (5191168365305634625) -->
-    <skip />
+    <string name="glance_error_layout_text_v2" msgid="5191168365305634625">"Hindi makapagpakita ng content"</string>
 </resources>
diff --git a/glance/glance-appwidget/src/main/res/values-uk/strings.xml b/glance/glance-appwidget/src/main/res/values-uk/strings.xml
index ecd6171..f873a04 100644
--- a/glance/glance-appwidget/src/main/res/values-uk/strings.xml
+++ b/glance/glance-appwidget/src/main/res/values-uk/strings.xml
@@ -19,6 +19,5 @@
     xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
     <string name="glance_error_layout_title" msgid="3631961919234443531"><b>"Помилка віджета додатка Glance"</b></string>
     <string name="glance_error_layout_text" msgid="2863935784364843033">"Щоб отримати точні відомості про помилку, скористайтеся командою "<b><tt>"adb logcat"</tt></b>" під час пошуку за запитом "<b><tt>"GlanceAppWidget"</tt></b></string>
-    <!-- no translation found for glance_error_layout_text_v2 (5191168365305634625) -->
-    <skip />
+    <string name="glance_error_layout_text_v2" msgid="5191168365305634625">"Не вдається відобразити контент"</string>
 </resources>
diff --git a/glance/glance-appwidget/src/main/res/values-uz/strings.xml b/glance/glance-appwidget/src/main/res/values-uz/strings.xml
index 71690c5..43c3c56 100644
--- a/glance/glance-appwidget/src/main/res/values-uz/strings.xml
+++ b/glance/glance-appwidget/src/main/res/values-uz/strings.xml
@@ -19,6 +19,5 @@
     xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
     <string name="glance_error_layout_title" msgid="3631961919234443531"><b>"Glance ilova vidjeti xatosi"</b></string>
     <string name="glance_error_layout_text" msgid="2863935784364843033">"Muayyan xatoni "<b><tt>"adb logcat"</tt></b>" yordamida tekshiring va "<b><tt>"GlanceAppWidget"</tt>" qatorini qidiring"</b></string>
-    <!-- no translation found for glance_error_layout_text_v2 (5191168365305634625) -->
-    <skip />
+    <string name="glance_error_layout_text_v2" msgid="5191168365305634625">"Kontent chiqmaydi"</string>
 </resources>
diff --git a/glance/glance-appwidget/src/main/res/values-vi/strings.xml b/glance/glance-appwidget/src/main/res/values-vi/strings.xml
index db87a72..c88ad6f 100644
--- a/glance/glance-appwidget/src/main/res/values-vi/strings.xml
+++ b/glance/glance-appwidget/src/main/res/values-vi/strings.xml
@@ -19,6 +19,5 @@
     xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
     <string name="glance_error_layout_title" msgid="3631961919234443531"><b>"Lỗi Glance App Widget (Tiện ích xem nhanh ứng dụng)"</b></string>
     <string name="glance_error_layout_text" msgid="2863935784364843033">"Xem đúng lỗi bằng cách dùng "<b><tt>"logcat adb"</tt></b>" để tìm "<b><tt>"GlanceAppWidget"</tt></b></string>
-    <!-- no translation found for glance_error_layout_text_v2 (5191168365305634625) -->
-    <skip />
+    <string name="glance_error_layout_text_v2" msgid="5191168365305634625">"Không hiện được nội dung"</string>
 </resources>
diff --git a/glance/glance-appwidget/src/main/res/values-zh-rHK/strings.xml b/glance/glance-appwidget/src/main/res/values-zh-rHK/strings.xml
index 5bcdb2b..38f190e 100644
--- a/glance/glance-appwidget/src/main/res/values-zh-rHK/strings.xml
+++ b/glance/glance-appwidget/src/main/res/values-zh-rHK/strings.xml
@@ -19,6 +19,5 @@
     xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
     <string name="glance_error_layout_title" msgid="3631961919234443531"><b>"Glance 應用程式小工具錯誤"</b></string>
     <string name="glance_error_layout_text" msgid="2863935784364843033">"使用 "<b><tt>"adb logcat"</tt></b>" 搜尋 "<b><tt>"GlanceAppWidget"</tt></b>",查看確實的錯誤資料"</string>
-    <!-- no translation found for glance_error_layout_text_v2 (5191168365305634625) -->
-    <skip />
+    <string name="glance_error_layout_text_v2" msgid="5191168365305634625">"無法顯示內容"</string>
 </resources>
diff --git a/glance/glance-appwidget/src/main/res/values-zh-rTW/strings.xml b/glance/glance-appwidget/src/main/res/values-zh-rTW/strings.xml
index 02605dc..cf1113d 100644
--- a/glance/glance-appwidget/src/main/res/values-zh-rTW/strings.xml
+++ b/glance/glance-appwidget/src/main/res/values-zh-rTW/strings.xml
@@ -19,6 +19,5 @@
     xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
     <string name="glance_error_layout_title" msgid="3631961919234443531"><b>"Glance 應用程式小工具錯誤"</b></string>
     <string name="glance_error_layout_text" msgid="2863935784364843033">"搜尋 "<b><tt>"GlanceAppWidget"</tt></b>" 時,你可以使用 "<b><tt>"adb logcat"</tt></b>" 查看確切的錯誤資訊"</string>
-    <!-- no translation found for glance_error_layout_text_v2 (5191168365305634625) -->
-    <skip />
+    <string name="glance_error_layout_text_v2" msgid="5191168365305634625">"無法顯示內容"</string>
 </resources>
diff --git a/gradle.properties b/gradle.properties
index 304a327..7194ebd 100644
--- a/gradle.properties
+++ b/gradle.properties
@@ -17,8 +17,7 @@
 # The following entries are workarounds
 # fullsdk-linux/**/package.xml -> b/291331139
 # androidx/compose/lint/common/build/libs/common.jar -> b/295395616
-# .konan/kotlin-native-prebuilt-linux-x86_64-1.9.10 -> https://youtrack.jetbrains.com/issue/KT-61154/
-org.gradle.configuration-cache.inputs.unsafe.ignore.file-system-checks=**/prebuilts/fullsdk-linux;**/prebuilts/fullsdk-linux/platforms/android-*/package.xml;**/androidx/compose/lint/common/build/libs/common.jar;**/.konan/kotlin-native-prebuilt-linux-x86_64-1.9.10/klib/common/stdlib;**/.konan/kotlin-native-prebuilt-linux-x86_64-1.9.10/konan/lib/*
+org.gradle.configuration-cache.inputs.unsafe.ignore.file-system-checks=**/prebuilts/fullsdk-linux;**/prebuilts/fullsdk-linux/platforms/android-*/package.xml;**/androidx/compose/lint/common/build/libs/common.jar
 
 android.lint.baselineOmitLineNumbers=true
 android.lint.printStackTrace=true
diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml
index 51d3b26..aa65f2a 100644
--- a/gradle/libs.versions.toml
+++ b/gradle/libs.versions.toml
@@ -14,12 +14,12 @@
 androidGradlePluginMin = "7.0.4"
 androidLintMin = "30.0.4"
 androidLintMinCompose = "30.0.0"
-androidxTestRunner = "1.6.0-alpha05"
-androidxTestRules = "1.6.0-alpha02"
-androidxTestMonitor = "1.7.0-alpha03"
-androidxTestCore = "1.6.0-alpha03"
-androidxTestExtJunit = "1.2.0-alpha02"
-androidxTestExtTruth = "1.6.0-alpha02"
+androidxTestRunner = "1.6.0-alpha06"
+androidxTestRules = "1.6.0-alpha03"
+androidxTestMonitor = "1.7.0-alpha04"
+androidxTestCore = "1.6.0-alpha05"
+androidxTestExtJunit = "1.2.0-alpha03"
+androidxTestExtTruth = "1.6.0-alpha03"
 annotationVersion = "1.7.0"
 atomicFu = "0.17.0"
 autoService = "1.0-rc6"
@@ -31,8 +31,8 @@
 dagger = "2.49"
 dexmaker = "2.28.3"
 dokka = "1.8.20-dev-214"
-espresso = "3.6.0-alpha02"
-espressoDevice = "1.0.0-alpha07"
+espresso = "3.6.0-alpha03"
+espressoDevice = "1.0.0-alpha08"
 grpc = "1.52.0"
 guavaJre = "31.1-jre"
 hilt = "2.49"
diff --git a/gradlew b/gradlew
index 11901b7e..64c7da7 100755
--- a/gradlew
+++ b/gradlew
@@ -303,14 +303,26 @@
   fi
 done
 
+raiseMemory=false
 if [[ " ${@} " =~ " -Pandroidx.highMemory " ]]; then
-    #Set the initial heap size to match the max heap size,
-    #by replacing a string like "-Xmx1g" with one like "-Xms1g -Xmx1g"
-    MAX_MEM=32g
-    ORG_GRADLE_JVMARGS="$(echo $ORG_GRADLE_JVMARGS | sed "s/-Xmx\([^ ]*\)/-Xms$MAX_MEM -Xmx$MAX_MEM/")"
+    raiseMemory=true
+fi
+if [[ " ${@} " =~ " -Pandroidx.lowMemory " ]]; then
+  if [ "$raiseMemory" == "true" ]; then
+    echo "androidx.lowMemory overriding androidx.highMemory"
+    echo
+  fi
+  raiseMemory=false
+fi
 
-    # Increase the compiler cache size: b/260643754 . Remove when updating to JDK 20 ( https://bugs.openjdk.org/browse/JDK-8295724 )
-    ORG_GRADLE_JVMARGS="$(echo $ORG_GRADLE_JVMARGS | sed "s|$| -XX:ReservedCodeCacheSize=576M|")"
+if [ "$raiseMemory" == "true" ]; then
+  # Set the initial heap size to match the max heap size,
+  # by replacing a string like "-Xmx1g" with one like "-Xms1g -Xmx1g"
+  MAX_MEM=32g
+  ORG_GRADLE_JVMARGS="$(echo $ORG_GRADLE_JVMARGS | sed "s/-Xmx\([^ ]*\)/-Xms$MAX_MEM -Xmx$MAX_MEM/")"
+
+  # Increase the compiler cache size: b/260643754 . Remove when updating to JDK 20 ( https://bugs.openjdk.org/browse/JDK-8295724 )
+  ORG_GRADLE_JVMARGS="$(echo $ORG_GRADLE_JVMARGS | sed "s|$| -XX:ReservedCodeCacheSize=576M|")"
 fi
 
 # check whether the user has requested profiling via yourkit
diff --git a/graphics/graphics-core/src/androidTest/java/androidx/graphics/CanvasBufferedRendererTests.kt b/graphics/graphics-core/src/androidTest/java/androidx/graphics/CanvasBufferedRendererTests.kt
index 9420dab..f77ccb5 100644
--- a/graphics/graphics-core/src/androidTest/java/androidx/graphics/CanvasBufferedRendererTests.kt
+++ b/graphics/graphics-core/src/androidTest/java/androidx/graphics/CanvasBufferedRendererTests.kt
@@ -324,11 +324,10 @@
         }
         assertNotNull(renderResult)
         assertEquals(SUCCESS, renderResult!!.status)
-        val fence = renderResult?.fence
-        assertNotNull(fence)
-        assertTrue(fence!!.isValid())
-        fence.awaitForever()
-        fence.close()
+        renderResult?.fence?.let { fence ->
+            fence.awaitForever()
+            fence.close()
+        }
 
         val hardwareBuffer = renderResult!!.hardwareBuffer
 
diff --git a/graphics/graphics-core/src/androidTest/java/androidx/graphics/lowlatency/GLFrontBufferedRendererTest.kt b/graphics/graphics-core/src/androidTest/java/androidx/graphics/lowlatency/GLFrontBufferedRendererTest.kt
index 463b140..c9cbc3f 100644
--- a/graphics/graphics-core/src/androidTest/java/androidx/graphics/lowlatency/GLFrontBufferedRendererTest.kt
+++ b/graphics/graphics-core/src/androidTest/java/androidx/graphics/lowlatency/GLFrontBufferedRendererTest.kt
@@ -1014,7 +1014,7 @@
     }
 
     @Test
-    @SdkSuppress(minSdkVersion = Build.VERSION_CODES.Q)
+    @SdkSuppress(minSdkVersion = Build.VERSION_CODES.Q, maxSdkVersion = 33) // maxSdk 33 b/315994268
     fun testRenderFrontBufferSeveralTimes() {
         val callbacks = object : GLFrontBufferedRenderer.Callback<Any> {
 
diff --git a/graphics/graphics-core/src/androidTest/java/androidx/graphics/lowlatency/SingleBufferedCanvasRendererTest.kt b/graphics/graphics-core/src/androidTest/java/androidx/graphics/lowlatency/SingleBufferedCanvasRendererTest.kt
index 1d745ca..a902170 100644
--- a/graphics/graphics-core/src/androidTest/java/androidx/graphics/lowlatency/SingleBufferedCanvasRendererTest.kt
+++ b/graphics/graphics-core/src/androidTest/java/androidx/graphics/lowlatency/SingleBufferedCanvasRendererTest.kt
@@ -37,6 +37,7 @@
 import org.junit.Assert.assertFalse
 import org.junit.Assert.assertNotNull
 import org.junit.Assert.assertTrue
+import org.junit.Ignore
 import org.junit.Test
 import org.junit.runner.RunWith
 
@@ -76,6 +77,7 @@
         )
     }
 
+    @Ignore // b/321800558
     @Test
     fun testRenderFrameRotate90() {
         testRenderWithTransform(
@@ -114,6 +116,7 @@
         )
     }
 
+    @Ignore // b/321800558
     @Test
     fun testRenderFrameRotate270() {
         testRenderWithTransform(
@@ -183,6 +186,7 @@
         }
     }
 
+    @Ignore // b/321800558
     @Test
     fun testCancelPending() {
         val transformer = BufferTransformer().apply {
diff --git a/graphics/graphics-core/src/androidTest/java/androidx/graphics/opengl/egl/EGLManagerTest.kt b/graphics/graphics-core/src/androidTest/java/androidx/graphics/opengl/egl/EGLManagerTest.kt
index 31fb4cd..a32525c 100644
--- a/graphics/graphics-core/src/androidTest/java/androidx/graphics/opengl/egl/EGLManagerTest.kt
+++ b/graphics/graphics-core/src/androidTest/java/androidx/graphics/opengl/egl/EGLManagerTest.kt
@@ -289,11 +289,13 @@
         }
     }
 
+    @SdkSuppress(maxSdkVersion = 33) // b/315169745
     @Test
     fun testSurfaceContentsWithBackBuffer() {
         verifySurfaceContentsWithWindowConfig()
     }
 
+    @SdkSuppress(maxSdkVersion = 33) // b/315169745
     @Test
     fun testSurfaceContentsWithFrontBuffer() {
         verifySurfaceContentsWithWindowConfig(true)
diff --git a/graphics/graphics-core/src/main/cpp/CMakeLists.txt b/graphics/graphics-core/src/main/cpp/CMakeLists.txt
index 5bd2dc3..bc21166 100644
--- a/graphics/graphics-core/src/main/cpp/CMakeLists.txt
+++ b/graphics/graphics-core/src/main/cpp/CMakeLists.txt
@@ -60,3 +60,7 @@
                        GLESv2
                        android)
 
+target_link_options(graphics-core
+                    PRIVATE
+                    "-Wl,-z,max-page-size=16384")
+
diff --git a/graphics/graphics-path/src/main/cpp/CMakeLists.txt b/graphics/graphics-path/src/main/cpp/CMakeLists.txt
index 8d99cce..722eb2d 100644
--- a/graphics/graphics-path/src/main/cpp/CMakeLists.txt
+++ b/graphics/graphics-path/src/main/cpp/CMakeLists.txt
@@ -14,5 +14,5 @@
 target_link_options(
     androidx.graphics.path
     PRIVATE
-    "-Wl,--version-script=${VERSION_SCRIPT}"
+    "-Wl,--version-script=${VERSION_SCRIPT},-z,max-page-size=16384"
 )
diff --git a/inspection/inspection-gradle-plugin/src/main/kotlin/androidx/inspection/gradle/InspectionPlugin.kt b/inspection/inspection-gradle-plugin/src/main/kotlin/androidx/inspection/gradle/InspectionPlugin.kt
index 7bcd452..2b5bf0a 100644
--- a/inspection/inspection-gradle-plugin/src/main/kotlin/androidx/inspection/gradle/InspectionPlugin.kt
+++ b/inspection/inspection-gradle-plugin/src/main/kotlin/androidx/inspection/gradle/InspectionPlugin.kt
@@ -102,7 +102,7 @@
         }
 
         project.apply(plugin = "com.google.protobuf")
-        project.plugins.all {
+        project.plugins.configureEach {
             if (it is ProtobufPlugin) {
                 val protobufExtension = project.extensions.getByType(ProtobufExtension::class.java)
                 protobufExtension.apply {
@@ -189,7 +189,7 @@
 
     generateProguardDetectionFile(libraryProject)
     val libExtension = libraryProject.extensions.getByType(LibraryExtension::class.java)
-    libExtension.libraryVariants.all { variant ->
+    libExtension.libraryVariants.configureEach { variant ->
         variant.packageLibraryProvider.configure { zip ->
             zip.from(consumeInspectorFiles)
             zip.rename {
@@ -259,7 +259,7 @@
 @ExperimentalStdlibApi
 private fun generateProguardDetectionFile(libraryProject: Project) {
     val libExtension = libraryProject.extensions.getByType(LibraryExtension::class.java)
-    libExtension.libraryVariants.all { variant ->
+    libExtension.libraryVariants.configureEach { variant ->
         libraryProject.registerGenerateProguardDetectionFileTask(variant)
     }
 }
diff --git a/inspection/inspection/src/main/native/CMakeLists.txt b/inspection/inspection/src/main/native/CMakeLists.txt
index 2a7f747..0897442 100644
--- a/inspection/inspection/src/main/native/CMakeLists.txt
+++ b/inspection/inspection/src/main/native/CMakeLists.txt
@@ -51,3 +51,4 @@
 target_include_directories(art_tooling
         PRIVATE ${CMAKE_CURRENT_BINARY_DIR}/external_jvmti)
 target_link_libraries(art_tooling ${log-lib})
+target_link_options(art_tooling PRIVATE "-Wl,-z,max-page-size=16384")
diff --git a/leanback/leanback-preference/src/main/res/color/lb_switch_compat_track_color.xml b/leanback/leanback-preference/src/main/res/color/lb_switch_compat_track_color.xml
new file mode 100644
index 0000000..e1213cc
--- /dev/null
+++ b/leanback/leanback-preference/src/main/res/color/lb_switch_compat_track_color.xml
@@ -0,0 +1,18 @@
+<?xml version="1.0" encoding="utf-8"?><!-- Copyright (C) 2014 The Android Open Source Project
+
+     Licensed under the Apache License, Version 2.0 (the "License");
+     you may not use this file except in compliance with the License.
+     You may obtain a copy of the License at
+
+          http://www.apache.org/licenses/LICENSE-2.0
+
+     Unless required by applicable law or agreed to in writing, software
+     distributed under the License is distributed on an "AS IS" BASIS,
+     WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+     See the License for the specific language governing permissions and
+     limitations under the License.
+-->
+
+<selector xmlns:android="http://schemas.android.com/apk/res/android">
+    <item android:alpha="?android:attr/disabledAlpha" android:color="@android:color/white" />
+</selector>
diff --git a/leanback/leanback-preference/src/main/res/color/lb_switch_compat_track_tint.xml b/leanback/leanback-preference/src/main/res/color/lb_switch_compat_track_tint.xml
new file mode 100644
index 0000000..c10a7f6e
--- /dev/null
+++ b/leanback/leanback-preference/src/main/res/color/lb_switch_compat_track_tint.xml
@@ -0,0 +1,23 @@
+<?xml version="1.0" encoding="utf-8"?><!-- Copyright (C) 2014 The Android Open Source Project
+
+     Licensed under the Apache License, Version 2.0 (the "License");
+     you may not use this file except in compliance with the License.
+     You may obtain a copy of the License at
+
+          http://www.apache.org/licenses/LICENSE-2.0
+
+     Unless required by applicable law or agreed to in writing, software
+     distributed under the License is distributed on an "AS IS" BASIS,
+     WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+     See the License for the specific language governing permissions and
+     limitations under the License.
+-->
+
+<selector xmlns:android="http://schemas.android.com/apk/res/android">
+    <item
+        android:state_enabled="false"
+        android:color="?android:attr/colorForeground"
+        android:alpha="?android:attr/disabledAlpha" />
+    <item android:state_checked="true" android:color="?android:attr/colorControlActivated" />
+    <item android:color="?android:attr/colorForeground" />
+</selector>
diff --git a/leanback/leanback-preference/src/main/res/drawable/leanback_switch_compat_track.xml b/leanback/leanback-preference/src/main/res/drawable/leanback_switch_compat_track.xml
new file mode 100644
index 0000000..92dd51c
--- /dev/null
+++ b/leanback/leanback-preference/src/main/res/drawable/leanback_switch_compat_track.xml
@@ -0,0 +1,29 @@
+<?xml version="1.0" encoding="utf-8"?><!-- Copyright (C) 2014 The Android Open Source Project
+
+     Licensed under the Apache License, Version 2.0 (the "License");
+     you may not use this file except in compliance with the License.
+     You may obtain a copy of the License at
+
+          http://www.apache.org/licenses/LICENSE-2.0
+
+     Unless required by applicable law or agreed to in writing, software
+     distributed under the License is distributed on an "AS IS" BASIS,
+     WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+     See the License for the specific language governing permissions and
+     limitations under the License.
+-->
+
+<layer-list xmlns:android="http://schemas.android.com/apk/res/android">
+    <item
+        android:end="4dp"
+        android:gravity="center_vertical|fill_horizontal"
+        android:start="4dp">
+        <shape
+            android:shape="rectangle"
+            android:tint="@color/lb_switch_compat_track_tint">
+            <corners android:radius="7dp" />
+            <size android:height="14dp" />
+            <solid android:color="@color/lb_switch_compat_track_color" />
+        </shape>
+    </item>
+</layer-list>
diff --git a/leanback/leanback-preference/src/main/res/layout/leanback_preference_widget_switch_compat.xml b/leanback/leanback-preference/src/main/res/layout/leanback_preference_widget_switch_compat.xml
new file mode 100644
index 0000000..b6c9662
--- /dev/null
+++ b/leanback/leanback-preference/src/main/res/layout/leanback_preference_widget_switch_compat.xml
@@ -0,0 +1,26 @@
+<?xml version="1.0" encoding="utf-8"?><!--
+  Copyright 2024 The Android Open Source Project
+
+  Licensed under the Apache License, Version 2.0 (the "License");
+  you may not use this file except in compliance with the License.
+  You may obtain a copy of the License at
+
+       http://www.apache.org/licenses/LICENSE-2.0
+
+  Unless required by applicable law or agreed to in writing, software
+  distributed under the License is distributed on an "AS IS" BASIS,
+  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+  See the License for the specific language governing permissions and
+  limitations under the License.
+  -->
+
+<androidx.appcompat.widget.SwitchCompat xmlns:android="http://schemas.android.com/apk/res/android"
+    xmlns:tools="http://schemas.android.com/tools"
+    tools:ignore="NewApi"
+    android:id="@+id/switchWidget"
+    android:layout_width="wrap_content"
+    android:layout_height="wrap_content"
+    style="?android:attr/switchStyle"
+    android:focusable="false"
+    android:clickable="false"
+    android:background="@null" />
diff --git a/leanback/leanback-preference/src/main/res/values-v28/themes.xml b/leanback/leanback-preference/src/main/res/values-v28/themes.xml
index dbac03b..d260a6a 100644
--- a/leanback/leanback-preference/src/main/res/values-v28/themes.xml
+++ b/leanback/leanback-preference/src/main/res/values-v28/themes.xml
@@ -88,10 +88,16 @@
 
         <item name="android:radioButtonStyle">@android:style/Widget.Material.CompoundButton.RadioButton</item>
 
-        <item name="android:switchStyle">@android:style/Widget.Material.CompoundButton.Switch</item>
+        <item name="android:switchStyle">@style/LeanbackSwitchCompatStyle</item>
 
         <item name="android:seekBarStyle">@android:style/Widget.Material.SeekBar</item>
 
     </style>
 
+    <!-- Extends Switch style to SwitchCompat. -->
+    <style name="LeanbackSwitchCompatStyle" parent="@android:style/Widget.Material.CompoundButton.Switch">
+        <item name="showText">false</item>
+        <item name="track">@drawable/leanback_switch_compat_track</item>
+    </style>
+
 </resources>
\ No newline at end of file
diff --git a/leanback/leanback-preference/src/main/res/values/styles.xml b/leanback/leanback-preference/src/main/res/values/styles.xml
index 3a773e5..ca08ff9 100644
--- a/leanback/leanback-preference/src/main/res/values/styles.xml
+++ b/leanback/leanback-preference/src/main/res/values/styles.xml
@@ -64,7 +64,7 @@
     </style>
 
     <style name="LeanbackPreference.SwitchPreferenceCompat">
-        <item name="android:widgetLayout">@layout/leanback_preference_widget_switch</item>
+        <item name="android:widgetLayout">@layout/leanback_preference_widget_switch_compat</item>
         <item name="android:switchTextOn">@string/v7_preference_on</item>
         <item name="android:switchTextOff">@string/v7_preference_off</item>
     </style>
diff --git a/leanback/leanback/src/main/res/values-zh-rCN/strings.xml b/leanback/leanback/src/main/res/values-zh-rCN/strings.xml
index 8c773e4..f6bb7f24d 100644
--- a/leanback/leanback/src/main/res/values-zh-rCN/strings.xml
+++ b/leanback/leanback/src/main/res/values-zh-rCN/strings.xml
@@ -36,8 +36,8 @@
     <string name="lb_playback_controls_more_actions" msgid="8730341244454469032">"更多操作"</string>
     <string name="lb_playback_controls_thumb_up" msgid="3458671378107738666">"取消选择顶操作"</string>
     <string name="lb_playback_controls_thumb_up_outline" msgid="1385865732502550659">"选择顶操作"</string>
-    <string name="lb_playback_controls_thumb_down" msgid="3544533410444618518">"取消选择踩操作"</string>
-    <string name="lb_playback_controls_thumb_down_outline" msgid="8475278766138652105">"选择踩操作"</string>
+    <string name="lb_playback_controls_thumb_down" msgid="3544533410444618518">"取消不喜欢"</string>
+    <string name="lb_playback_controls_thumb_down_outline" msgid="8475278766138652105">"选择不喜欢"</string>
     <string name="lb_playback_controls_repeat_none" msgid="1614290959784265209">"不重复播放"</string>
     <string name="lb_playback_controls_repeat_all" msgid="8429099206716245199">"重复播放全部"</string>
     <string name="lb_playback_controls_repeat_one" msgid="676658705837320560">"重复播放一项"</string>
diff --git a/libraryversions.toml b/libraryversions.toml
index cade605..0d49506 100644
--- a/libraryversions.toml
+++ b/libraryversions.toml
@@ -2,7 +2,6 @@
 ACTIVITY = "1.9.0-alpha03"
 ANNOTATION = "1.8.0-alpha01"
 ANNOTATION_EXPERIMENTAL = "1.4.0-rc01"
-ANNOTATION_REPLACEWITH = "1.0.0-alpha01"
 APPACTIONS_BUILTINTYPES = "1.0.0-alpha01"
 APPACTIONS_INTERACTION = "1.0.0-alpha01"
 APPCOMPAT = "1.7.0-alpha04"
@@ -23,11 +22,11 @@
 CARDVIEW = "1.1.0-alpha01"
 CAR_APP = "1.7.0-alpha01"
 COLLECTION = "1.5.0-alpha01"
-COMPOSE = "1.7.0-alpha01"
+COMPOSE = "1.7.0-alpha02"
 COMPOSE_COMPILER = "1.5.8"
 COMPOSE_MATERIAL3 = "1.3.0-alpha01"
-COMPOSE_MATERIAL3_ADAPTIVE = "1.0.0-alpha05"
-COMPOSE_MATERIAL3_ADAPTIVE_NAVIGATION_SUITE = "1.0.0-alpha02"
+COMPOSE_MATERIAL3_ADAPTIVE = "1.0.0-alpha06"
+COMPOSE_MATERIAL3_ADAPTIVE_NAVIGATION_SUITE = "1.0.0-alpha03"
 COMPOSE_MATERIAL3_COMMON = "1.0.0-alpha01"
 COMPOSE_RUNTIME_TRACING = "1.0.0-beta01"
 CONSTRAINTLAYOUT = "2.2.0-alpha13"
@@ -35,7 +34,7 @@
 CONSTRAINTLAYOUT_CORE = "1.1.0-alpha13"
 CONTENTPAGER = "1.1.0-alpha01"
 COORDINATORLAYOUT = "1.3.0-alpha02"
-CORE = "1.13.0-alpha04"
+CORE = "1.13.0-alpha05"
 CORE_ANIMATION = "1.0.0-rc01"
 CORE_ANIMATION_TESTING = "1.0.0-rc01"
 CORE_APPDIGEST = "1.0.0-alpha01"
@@ -64,7 +63,7 @@
 EMOJI2 = "1.5.0-alpha01"
 ENTERPRISE = "1.1.0-rc01"
 EXIFINTERFACE = "1.4.0-alpha01"
-FRAGMENT = "1.7.0-alpha09"
+FRAGMENT = "1.7.0-alpha10"
 FUTURES = "1.2.0-alpha02"
 GLANCE = "1.1.0-alpha01"
 GLANCE_PREVIEW = "1.0.0-alpha06"
@@ -78,9 +77,9 @@
 HEALTH_CONNECT = "1.1.0-alpha07"
 HEALTH_SERVICES_CLIENT = "1.1.0-alpha02"
 HEIFWRITER = "1.1.0-alpha03"
-HILT = "1.2.0-beta01"
-HILT_NAVIGATION = "1.2.0-beta01"
-HILT_NAVIGATION_COMPOSE = "1.2.0-beta01"
+HILT = "1.2.0-rc01"
+HILT_NAVIGATION = "1.2.0-rc01"
+HILT_NAVIGATION_COMPOSE = "1.2.0-rc01"
 INPUT_MOTIONPREDICTION = "1.0.0-beta03"
 INSPECTION = "1.0.0"
 INTERPOLATOR = "1.1.0-alpha01"
@@ -95,6 +94,7 @@
 LIBYUV = "0.1.0-dev01"
 LIFECYCLE = "2.8.0-alpha01"
 LIFECYCLE_EXTENSIONS = "2.2.0"
+LINT = "1.0.0-alpha01"
 LOADER = "1.2.0-alpha01"
 MEDIA = "1.7.0-rc01"
 MEDIA2 = "1.3.0-rc01"
@@ -111,7 +111,7 @@
 PRIVACYSANDBOX_ADS = "1.1.0-beta04"
 PRIVACYSANDBOX_PLUGINS = "1.0.0-alpha03"
 PRIVACYSANDBOX_SDKRUNTIME = "1.0.0-alpha12"
-PRIVACYSANDBOX_TOOLS = "1.0.0-alpha06"
+PRIVACYSANDBOX_TOOLS = "1.0.0-alpha07"
 PRIVACYSANDBOX_UI = "1.0.0-alpha07"
 PROFILEINSTALLER = "1.4.0-alpha01"
 RECOMMENDATION = "1.1.0-alpha01"
@@ -154,8 +154,8 @@
 VIEWPAGER = "1.1.0-alpha02"
 VIEWPAGER2 = "1.1.0-beta03"
 WEAR = "1.4.0-alpha01"
-WEAR_COMPOSE = "1.4.0-alpha01"
-WEAR_COMPOSE_MATERIAL3 = "1.0.0-alpha16"
+WEAR_COMPOSE = "1.4.0-alpha02"
+WEAR_COMPOSE_MATERIAL3 = "1.0.0-alpha17"
 WEAR_INPUT = "1.2.0-alpha03"
 WEAR_INPUT_TESTING = "1.2.0-alpha03"
 WEAR_ONGOING = "1.1.0-alpha02"
@@ -245,6 +245,7 @@
 LIBYUV = { group = "libyuv", atomicGroupVersion = "versions.LIBYUV" }
 LIFECYCLE = { group = "androidx.lifecycle", atomicGroupVersion = "versions.LIFECYCLE" }
 LIFECYCLE_EXTENSIONS = { group = "androidx.lifecycle", atomicGroupVersion = "versions.LIFECYCLE_EXTENSIONS", overrideInclude = [ ":lifecycle:lifecycle-extensions" ] }
+LINT = { group = "androidx.lint", atomicGroupVersion = "versions.LINT" }
 LOADER = { group = "androidx.loader", atomicGroupVersion = "versions.LOADER" }
 MEDIA = { group = "androidx.media" }
 MEDIA2 = { group = "androidx.media2", atomicGroupVersion = "versions.MEDIA2" }
diff --git a/lifecycle/lifecycle-common/build.gradle b/lifecycle/lifecycle-common/build.gradle
index bc25ff6..b145a44 100644
--- a/lifecycle/lifecycle-common/build.gradle
+++ b/lifecycle/lifecycle-common/build.gradle
@@ -21,26 +21,85 @@
  * Please use that script when creating a new project, rather than copying an existing project and
  * modifying its settings.
  */
+import androidx.build.KmpPlatformsKt
+import androidx.build.PlatformIdentifier
 import androidx.build.Publish
-import org.jetbrains.kotlin.gradle.tasks.KotlinCompile
+import org.jetbrains.kotlin.konan.target.Family
 
 plugins {
     id("AndroidXPlugin")
-    id("java-library")
-    id("kotlin")
 }
 
-dependencies {
-    api("androidx.annotation:annotation:1.1.0")
-    api(libs.kotlinStdlib)
-    api(libs.kotlinCoroutinesCore)
-    api(libs.kotlinCoroutinesAndroid) {
-        exclude group: 'org.jetbrains.kotlin', module: 'kotlin-stdlib-jdk7'
-        exclude group: 'org.jetbrains.kotlin', module: 'kotlin-stdlib-jdk8'
-    }
+def macEnabled = KmpPlatformsKt.enableMac(project)
+def linuxEnabled = KmpPlatformsKt.enableLinux(project)
 
-    testImplementation(libs.junit)
-    testImplementation(libs.mockitoCore4)
+androidXMultiplatform {
+    jvm {
+        withJava()
+    }
+    mac()
+    linux()
+    ios()
+
+    defaultPlatform(PlatformIdentifier.JVM)
+
+    sourceSets {
+        commonMain {
+            dependencies {
+                api(libs.kotlinStdlib)
+                api(libs.kotlinCoroutinesCore)
+                api(project(":annotation:annotation"))
+            }
+        }
+
+        jvmMain {
+            dependsOn(commonMain)
+        }
+
+        jvmTest {
+            dependencies {
+                implementation(libs.junit)
+                implementation(libs.mockitoCore4)
+            }
+        }
+
+        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 == org.jetbrains.kotlin.gradle.plugin.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}")
+                    }
+                }
+                target.compilations["test"].defaultSourceSet {
+                    dependsOn(nativeTest)
+                }
+            }
+        }
+    }
 }
 
 androidx {
diff --git a/lifecycle/lifecycle-common/src/main/java/androidx/lifecycle/DefaultLifecycleObserver.kt b/lifecycle/lifecycle-common/src/commonMain/kotlin/androidx.lifecycle/DefaultLifecycleObserver.kt
similarity index 100%
rename from lifecycle/lifecycle-common/src/main/java/androidx/lifecycle/DefaultLifecycleObserver.kt
rename to lifecycle/lifecycle-common/src/commonMain/kotlin/androidx.lifecycle/DefaultLifecycleObserver.kt
diff --git a/lifecycle/lifecycle-common/src/main/java/androidx/lifecycle/DefaultLifecycleObserverAdapter.kt b/lifecycle/lifecycle-common/src/commonMain/kotlin/androidx.lifecycle/DefaultLifecycleObserverAdapter.kt
similarity index 100%
rename from lifecycle/lifecycle-common/src/main/java/androidx/lifecycle/DefaultLifecycleObserverAdapter.kt
rename to lifecycle/lifecycle-common/src/commonMain/kotlin/androidx.lifecycle/DefaultLifecycleObserverAdapter.kt
diff --git a/lifecycle/lifecycle-common/src/main/java/androidx/lifecycle/Lifecycle.kt b/lifecycle/lifecycle-common/src/commonMain/kotlin/androidx.lifecycle/Lifecycle.kt
similarity index 82%
rename from lifecycle/lifecycle-common/src/main/java/androidx/lifecycle/Lifecycle.kt
rename to lifecycle/lifecycle-common/src/commonMain/kotlin/androidx.lifecycle/Lifecycle.kt
index 66ec94d..3812ec2 100644
--- a/lifecycle/lifecycle-common/src/main/java/androidx/lifecycle/Lifecycle.kt
+++ b/lifecycle/lifecycle-common/src/commonMain/kotlin/androidx.lifecycle/Lifecycle.kt
@@ -17,12 +17,10 @@
 
 import androidx.annotation.MainThread
 import androidx.annotation.RestrictTo
-import androidx.lifecycle.Lifecycle.Event
-import java.util.concurrent.atomic.AtomicReference
 import kotlin.coroutines.CoroutineContext
+import kotlin.jvm.JvmStatic
 import kotlinx.coroutines.CoroutineScope
 import kotlinx.coroutines.Dispatchers
-import kotlinx.coroutines.Job
 import kotlinx.coroutines.SupervisorJob
 import kotlinx.coroutines.cancel
 import kotlinx.coroutines.channels.awaitClose
@@ -61,7 +59,7 @@
     @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
     @get:RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
     @set:RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
-    public var internalScopeRef: AtomicReference<Any> = AtomicReference<Any>()
+    public var internalScopeRef: AtomicReference<Any?> = AtomicReference(null)
 
     /**
      * Adds a LifecycleObserver that will be notified when the LifecycleOwner changes
@@ -305,6 +303,12 @@
     }
 }
 
+@RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
+public expect class AtomicReference<V>(value: V) {
+    public fun get(): V
+    public fun compareAndSet(expect: V, newValue: V): Boolean
+}
+
 /**
  * [CoroutineScope] tied to this [Lifecycle].
  *
@@ -336,69 +340,9 @@
  * [Dispatchers.Main.immediate][kotlinx.coroutines.MainCoroutineDispatcher.immediate]
  *
  * This scope will be cancelled when the [Lifecycle] is destroyed.
- *
- * This scope provides specialised versions of `launch`: [launchWhenCreated], [launchWhenStarted],
- * [launchWhenResumed]
  */
-public abstract class LifecycleCoroutineScope internal constructor() : CoroutineScope {
+public expect abstract class LifecycleCoroutineScope internal constructor() : CoroutineScope {
     internal abstract val lifecycle: Lifecycle
-
-    /**
-     * Launches and runs the given block when the [Lifecycle] controlling this
-     * [LifecycleCoroutineScope] is at least in [Lifecycle.State.CREATED] state.
-     *
-     * The returned [Job] will be cancelled when the [Lifecycle] is destroyed.
-     *
-     * @see Lifecycle.whenCreated
-     * @see Lifecycle.coroutineScope
-     */
-    @Deprecated(
-        message = "launchWhenCreated is deprecated as it can lead to wasted resources " +
-            "in some cases. Replace with suspending repeatOnLifecycle to run the block whenever " +
-            "the Lifecycle state is at least Lifecycle.State.CREATED."
-    )
-    @Suppress("DEPRECATION")
-    public fun launchWhenCreated(block: suspend CoroutineScope.() -> Unit): Job = launch {
-        lifecycle.whenCreated(block)
-    }
-
-    /**
-     * Launches and runs the given block when the [Lifecycle] controlling this
-     * [LifecycleCoroutineScope] is at least in [Lifecycle.State.STARTED] state.
-     *
-     * The returned [Job] will be cancelled when the [Lifecycle] is destroyed.
-     *
-     * @see Lifecycle.whenStarted
-     * @see Lifecycle.coroutineScope
-     */
-    @Deprecated(
-        message = "launchWhenStarted is deprecated as it can lead to wasted resources " +
-            "in some cases. Replace with suspending repeatOnLifecycle to run the block whenever " +
-            "the Lifecycle state is at least Lifecycle.State.STARTED."
-    )
-    @Suppress("DEPRECATION")
-    public fun launchWhenStarted(block: suspend CoroutineScope.() -> Unit): Job = launch {
-        lifecycle.whenStarted(block)
-    }
-
-    /**
-     * Launches and runs the given block when the [Lifecycle] controlling this
-     * [LifecycleCoroutineScope] is at least in [Lifecycle.State.RESUMED] state.
-     *
-     * The returned [Job] will be cancelled when the [Lifecycle] is destroyed.
-     *
-     * @see Lifecycle.whenResumed
-     * @see Lifecycle.coroutineScope
-     */
-    @Deprecated(
-        message = "launchWhenResumed is deprecated as it can lead to wasted resources " +
-            "in some cases. Replace with suspending repeatOnLifecycle to run the block whenever " +
-            "the Lifecycle state is at least Lifecycle.State.RESUMED."
-    )
-    @Suppress("DEPRECATION")
-    public fun launchWhenResumed(block: suspend CoroutineScope.() -> Unit): Job = launch {
-        lifecycle.whenResumed(block)
-    }
 }
 
 internal class LifecycleCoroutineScopeImpl(
@@ -433,9 +377,9 @@
 }
 
 /**
- * Creates a [Flow] of [Event]s containing values dispatched by this [Lifecycle].
+ * Creates a [Flow] of [Lifecycle.Event]s containing values dispatched by this [Lifecycle].
  */
-public val Lifecycle.eventFlow: Flow<Event>
+public val Lifecycle.eventFlow: Flow<Lifecycle.Event>
     get() = callbackFlow {
         val observer = LifecycleEventObserver { _, event ->
             trySend(event)
diff --git a/lifecycle/lifecycle-common/src/main/java/androidx/lifecycle/LifecycleEventObserver.kt b/lifecycle/lifecycle-common/src/commonMain/kotlin/androidx.lifecycle/LifecycleEventObserver.kt
similarity index 100%
rename from lifecycle/lifecycle-common/src/main/java/androidx/lifecycle/LifecycleEventObserver.kt
rename to lifecycle/lifecycle-common/src/commonMain/kotlin/androidx.lifecycle/LifecycleEventObserver.kt
diff --git a/lifecycle/lifecycle-common/src/main/java/androidx/lifecycle/LifecycleObserver.kt b/lifecycle/lifecycle-common/src/commonMain/kotlin/androidx.lifecycle/LifecycleObserver.kt
similarity index 100%
rename from lifecycle/lifecycle-common/src/main/java/androidx/lifecycle/LifecycleObserver.kt
rename to lifecycle/lifecycle-common/src/commonMain/kotlin/androidx.lifecycle/LifecycleObserver.kt
diff --git a/lifecycle/lifecycle-common/src/main/java/androidx/lifecycle/LifecycleOwner.kt b/lifecycle/lifecycle-common/src/commonMain/kotlin/androidx.lifecycle/LifecycleOwner.kt
similarity index 100%
rename from lifecycle/lifecycle-common/src/main/java/androidx/lifecycle/LifecycleOwner.kt
rename to lifecycle/lifecycle-common/src/commonMain/kotlin/androidx.lifecycle/LifecycleOwner.kt
diff --git a/lifecycle/lifecycle-common/src/commonMain/kotlin/androidx.lifecycle/Lifecycling.kt b/lifecycle/lifecycle-common/src/commonMain/kotlin/androidx.lifecycle/Lifecycling.kt
new file mode 100644
index 0000000..1bca08b
--- /dev/null
+++ b/lifecycle/lifecycle-common/src/commonMain/kotlin/androidx.lifecycle/Lifecycling.kt
@@ -0,0 +1,32 @@
+/*
+ * Copyright (C) 2017 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package androidx.lifecycle
+
+import androidx.annotation.RestrictTo
+
+/**
+ * Internal class to handle lifecycle conversion etc.
+ */
+@RestrictTo(RestrictTo.Scope.LIBRARY_GROUP_PREFIX)
+public expect object Lifecycling {
+
+    public fun lifecycleEventObserver(`object`: Any): LifecycleEventObserver
+
+    /**
+     * Create a name for an adapter class.
+     */
+    public fun getAdapterName(className: String): String
+}
diff --git a/lifecycle/lifecycle-common/src/main/baseline-prof.txt b/lifecycle/lifecycle-common/src/jvmMain/baseline-prof.txt
similarity index 100%
rename from lifecycle/lifecycle-common/src/main/baseline-prof.txt
rename to lifecycle/lifecycle-common/src/jvmMain/baseline-prof.txt
diff --git a/lifecycle/lifecycle-common/src/main/java/androidx/lifecycle/ClassesInfoCache.java b/lifecycle/lifecycle-common/src/jvmMain/java/androidx/lifecycle/ClassesInfoCache.java
similarity index 100%
rename from lifecycle/lifecycle-common/src/main/java/androidx/lifecycle/ClassesInfoCache.java
rename to lifecycle/lifecycle-common/src/jvmMain/java/androidx/lifecycle/ClassesInfoCache.java
diff --git a/lifecycle/lifecycle-common/src/main/java/androidx/lifecycle/GenericLifecycleObserver.java b/lifecycle/lifecycle-common/src/jvmMain/java/androidx/lifecycle/GenericLifecycleObserver.java
similarity index 100%
rename from lifecycle/lifecycle-common/src/main/java/androidx/lifecycle/GenericLifecycleObserver.java
rename to lifecycle/lifecycle-common/src/jvmMain/java/androidx/lifecycle/GenericLifecycleObserver.java
diff --git a/lifecycle/lifecycle-common/src/main/java/androidx/lifecycle/OnLifecycleEvent.java b/lifecycle/lifecycle-common/src/jvmMain/java/androidx/lifecycle/OnLifecycleEvent.java
similarity index 100%
rename from lifecycle/lifecycle-common/src/main/java/androidx/lifecycle/OnLifecycleEvent.java
rename to lifecycle/lifecycle-common/src/jvmMain/java/androidx/lifecycle/OnLifecycleEvent.java
diff --git a/lifecycle/lifecycle-common/src/main/java/androidx/lifecycle/ReflectiveGenericLifecycleObserver.java b/lifecycle/lifecycle-common/src/jvmMain/java/androidx/lifecycle/ReflectiveGenericLifecycleObserver.java
similarity index 100%
rename from lifecycle/lifecycle-common/src/main/java/androidx/lifecycle/ReflectiveGenericLifecycleObserver.java
rename to lifecycle/lifecycle-common/src/jvmMain/java/androidx/lifecycle/ReflectiveGenericLifecycleObserver.java
diff --git a/lifecycle/lifecycle-common/src/main/java/androidx/lifecycle/CompositeGeneratedAdaptersObserver.kt b/lifecycle/lifecycle-common/src/jvmMain/kotlin/androidx/lifecycle/CompositeGeneratedAdaptersObserver.jvm.kt
similarity index 100%
rename from lifecycle/lifecycle-common/src/main/java/androidx/lifecycle/CompositeGeneratedAdaptersObserver.kt
rename to lifecycle/lifecycle-common/src/jvmMain/kotlin/androidx/lifecycle/CompositeGeneratedAdaptersObserver.jvm.kt
diff --git a/lifecycle/lifecycle-common/src/main/java/androidx/lifecycle/DispatchQueue.kt b/lifecycle/lifecycle-common/src/jvmMain/kotlin/androidx/lifecycle/DispatchQueue.jvm.kt
similarity index 100%
rename from lifecycle/lifecycle-common/src/main/java/androidx/lifecycle/DispatchQueue.kt
rename to lifecycle/lifecycle-common/src/jvmMain/kotlin/androidx/lifecycle/DispatchQueue.jvm.kt
diff --git a/lifecycle/lifecycle-common/src/main/java/androidx/lifecycle/GeneratedAdapter.kt b/lifecycle/lifecycle-common/src/jvmMain/kotlin/androidx/lifecycle/GeneratedAdapter.jvm.kt
similarity index 100%
rename from lifecycle/lifecycle-common/src/main/java/androidx/lifecycle/GeneratedAdapter.kt
rename to lifecycle/lifecycle-common/src/jvmMain/kotlin/androidx/lifecycle/GeneratedAdapter.jvm.kt
diff --git a/lifecycle/lifecycle-common/src/jvmMain/kotlin/androidx/lifecycle/Lifecycle.jvm.kt b/lifecycle/lifecycle-common/src/jvmMain/kotlin/androidx/lifecycle/Lifecycle.jvm.kt
new file mode 100644
index 0000000..cf33d38
--- /dev/null
+++ b/lifecycle/lifecycle-common/src/jvmMain/kotlin/androidx/lifecycle/Lifecycle.jvm.kt
@@ -0,0 +1,95 @@
+/*
+ * Copyright (C) 2017 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package androidx.lifecycle
+
+import java.util.concurrent.atomic.AtomicReference
+import kotlinx.coroutines.CoroutineScope
+import kotlinx.coroutines.Job
+import kotlinx.coroutines.launch
+
+@Suppress("ACTUAL_WITHOUT_EXPECT") // https://youtrack.jetbrains.com/issue/KT-37316
+public actual typealias AtomicReference<V> = AtomicReference<V>
+
+/**
+ * [CoroutineScope] tied to a [Lifecycle] and
+ * [Dispatchers.Main.immediate][kotlinx.coroutines.MainCoroutineDispatcher.immediate]
+ *
+ * This scope will be cancelled when the [Lifecycle] is destroyed.
+ *
+ * This scope provides specialised versions of `launch`: [launchWhenCreated], [launchWhenStarted],
+ * [launchWhenResumed]
+ */
+public actual abstract class LifecycleCoroutineScope internal actual constructor() :
+    CoroutineScope {
+    internal actual abstract val lifecycle: Lifecycle
+
+    /**
+     * Launches and runs the given block when the [Lifecycle] controlling this
+     * [LifecycleCoroutineScope] is at least in [Lifecycle.State.CREATED] state.
+     *
+     * The returned [Job] will be cancelled when the [Lifecycle] is destroyed.
+     *
+     * @see Lifecycle.whenCreated
+     * @see Lifecycle.coroutineScope
+     */
+    @Deprecated(
+        message = "launchWhenCreated is deprecated as it can lead to wasted resources " +
+            "in some cases. Replace with suspending repeatOnLifecycle to run the block whenever " +
+            "the Lifecycle state is at least Lifecycle.State.CREATED."
+    )
+    @Suppress("DEPRECATION")
+    public fun launchWhenCreated(block: suspend CoroutineScope.() -> Unit): Job = launch {
+        lifecycle.whenCreated(block)
+    }
+
+    /**
+     * Launches and runs the given block when the [Lifecycle] controlling this
+     * [LifecycleCoroutineScope] is at least in [Lifecycle.State.STARTED] state.
+     *
+     * The returned [Job] will be cancelled when the [Lifecycle] is destroyed.
+     *
+     * @see Lifecycle.whenStarted
+     * @see Lifecycle.coroutineScope
+     */
+    @Deprecated(
+        message = "launchWhenStarted is deprecated as it can lead to wasted resources " +
+            "in some cases. Replace with suspending repeatOnLifecycle to run the block whenever " +
+            "the Lifecycle state is at least Lifecycle.State.STARTED."
+    )
+    @Suppress("DEPRECATION")
+    public fun launchWhenStarted(block: suspend CoroutineScope.() -> Unit): Job = launch {
+        lifecycle.whenStarted(block)
+    }
+
+    /**
+     * Launches and runs the given block when the [Lifecycle] controlling this
+     * [LifecycleCoroutineScope] is at least in [Lifecycle.State.RESUMED] state.
+     *
+     * The returned [Job] will be cancelled when the [Lifecycle] is destroyed.
+     *
+     * @see Lifecycle.whenResumed
+     * @see Lifecycle.coroutineScope
+     */
+    @Deprecated(
+        message = "launchWhenResumed is deprecated as it can lead to wasted resources " +
+            "in some cases. Replace with suspending repeatOnLifecycle to run the block whenever " +
+            "the Lifecycle state is at least Lifecycle.State.RESUMED."
+    )
+    @Suppress("DEPRECATION")
+    public fun launchWhenResumed(block: suspend CoroutineScope.() -> Unit): Job = launch {
+        lifecycle.whenResumed(block)
+    }
+}
diff --git a/lifecycle/lifecycle-common/src/main/java/androidx/lifecycle/LifecycleController.kt b/lifecycle/lifecycle-common/src/jvmMain/kotlin/androidx/lifecycle/LifecycleController.jvm.kt
similarity index 100%
rename from lifecycle/lifecycle-common/src/main/java/androidx/lifecycle/LifecycleController.kt
rename to lifecycle/lifecycle-common/src/jvmMain/kotlin/androidx/lifecycle/LifecycleController.jvm.kt
diff --git a/lifecycle/lifecycle-common/src/main/java/androidx/lifecycle/Lifecycling.kt b/lifecycle/lifecycle-common/src/jvmMain/kotlin/androidx/lifecycle/Lifecycling.jvm.kt
similarity index 96%
rename from lifecycle/lifecycle-common/src/main/java/androidx/lifecycle/Lifecycling.kt
rename to lifecycle/lifecycle-common/src/jvmMain/kotlin/androidx/lifecycle/Lifecycling.jvm.kt
index 51efb21..1c39212 100644
--- a/lifecycle/lifecycle-common/src/main/java/androidx/lifecycle/Lifecycling.kt
+++ b/lifecycle/lifecycle-common/src/jvmMain/kotlin/androidx/lifecycle/Lifecycling.jvm.kt
@@ -23,7 +23,7 @@
  * Internal class to handle lifecycle conversion etc.
  */
 @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP_PREFIX)
-public object Lifecycling {
+public actual object Lifecycling {
     private const val REFLECTIVE_CALLBACK = 1
     private const val GENERATED_CALLBACK = 2
     private val callbackCache: MutableMap<Class<*>, Int> = HashMap()
@@ -32,7 +32,7 @@
 
     @JvmStatic
     @Suppress("DEPRECATION")
-    public fun lifecycleEventObserver(`object`: Any): LifecycleEventObserver {
+    public actual fun lifecycleEventObserver(`object`: Any): LifecycleEventObserver {
         val isLifecycleEventObserver = `object` is LifecycleEventObserver
         val isDefaultLifecycleObserver = `object` is DefaultLifecycleObserver
         if (isLifecycleEventObserver && isDefaultLifecycleObserver) {
@@ -170,7 +170,7 @@
      * Create a name for an adapter class.
      */
     @JvmStatic
-    public fun getAdapterName(className: String): String {
+    public actual fun getAdapterName(className: String): String {
         return className.replace(".", "_") + "_LifecycleAdapter"
     }
 }
diff --git a/lifecycle/lifecycle-common/src/main/java/androidx/lifecycle/MethodCallsLogger.kt b/lifecycle/lifecycle-common/src/jvmMain/kotlin/androidx/lifecycle/MethodCallsLogger.jvm.kt
similarity index 100%
rename from lifecycle/lifecycle-common/src/main/java/androidx/lifecycle/MethodCallsLogger.kt
rename to lifecycle/lifecycle-common/src/jvmMain/kotlin/androidx/lifecycle/MethodCallsLogger.jvm.kt
diff --git a/lifecycle/lifecycle-common/src/main/java/androidx/lifecycle/PausingDispatcher.kt b/lifecycle/lifecycle-common/src/jvmMain/kotlin/androidx/lifecycle/PausingDispatcher.jvm.kt
similarity index 98%
rename from lifecycle/lifecycle-common/src/main/java/androidx/lifecycle/PausingDispatcher.kt
rename to lifecycle/lifecycle-common/src/jvmMain/kotlin/androidx/lifecycle/PausingDispatcher.jvm.kt
index 7b388ed..fd67635 100644
--- a/lifecycle/lifecycle-common/src/main/java/androidx/lifecycle/PausingDispatcher.kt
+++ b/lifecycle/lifecycle-common/src/jvmMain/kotlin/androidx/lifecycle/PausingDispatcher.jvm.kt
@@ -14,6 +14,8 @@
  * limitations under the License.
  */
 
+@file:JvmName("PausingDispatcherKt")
+
 package androidx.lifecycle
 
 import kotlin.coroutines.CoroutineContext
diff --git a/lifecycle/lifecycle-common/src/main/java/androidx/lifecycle/SingleGeneratedAdapterObserver.kt b/lifecycle/lifecycle-common/src/jvmMain/kotlin/androidx/lifecycle/SingleGeneratedAdapterObserver.jvm.kt
similarity index 100%
rename from lifecycle/lifecycle-common/src/main/java/androidx/lifecycle/SingleGeneratedAdapterObserver.kt
rename to lifecycle/lifecycle-common/src/jvmMain/kotlin/androidx/lifecycle/SingleGeneratedAdapterObserver.jvm.kt
diff --git a/lifecycle/lifecycle-common/src/test/java/androidx/lifecycle/DefaultLifecycleObserverTest.kt b/lifecycle/lifecycle-common/src/jvmTest/java/androidx/lifecycle/DefaultLifecycleObserverTest.kt
similarity index 100%
rename from lifecycle/lifecycle-common/src/test/java/androidx/lifecycle/DefaultLifecycleObserverTest.kt
rename to lifecycle/lifecycle-common/src/jvmTest/java/androidx/lifecycle/DefaultLifecycleObserverTest.kt
diff --git a/lifecycle/lifecycle-common/src/test/java/androidx/lifecycle/LifecyclingTest.kt b/lifecycle/lifecycle-common/src/jvmTest/java/androidx/lifecycle/LifecyclingTest.kt
similarity index 100%
rename from lifecycle/lifecycle-common/src/test/java/androidx/lifecycle/LifecyclingTest.kt
rename to lifecycle/lifecycle-common/src/jvmTest/java/androidx/lifecycle/LifecyclingTest.kt
diff --git a/lifecycle/lifecycle-common/src/test/java/androidx/lifecycle/ReflectiveGenericLifecycleObserverTest.kt b/lifecycle/lifecycle-common/src/jvmTest/java/androidx/lifecycle/ReflectiveGenericLifecycleObserverTest.kt
similarity index 100%
rename from lifecycle/lifecycle-common/src/test/java/androidx/lifecycle/ReflectiveGenericLifecycleObserverTest.kt
rename to lifecycle/lifecycle-common/src/jvmTest/java/androidx/lifecycle/ReflectiveGenericLifecycleObserverTest.kt
diff --git a/lifecycle/lifecycle-common/src/test/java/androidx/lifecycle/observers/Base.kt b/lifecycle/lifecycle-common/src/jvmTest/java/androidx/lifecycle/observers/Base.kt
similarity index 100%
rename from lifecycle/lifecycle-common/src/test/java/androidx/lifecycle/observers/Base.kt
rename to lifecycle/lifecycle-common/src/jvmTest/java/androidx/lifecycle/observers/Base.kt
diff --git a/lifecycle/lifecycle-common/src/test/java/androidx/lifecycle/observers/Base_LifecycleAdapter.kt b/lifecycle/lifecycle-common/src/jvmTest/java/androidx/lifecycle/observers/Base_LifecycleAdapter.kt
similarity index 100%
rename from lifecycle/lifecycle-common/src/test/java/androidx/lifecycle/observers/Base_LifecycleAdapter.kt
rename to lifecycle/lifecycle-common/src/jvmTest/java/androidx/lifecycle/observers/Base_LifecycleAdapter.kt
diff --git a/lifecycle/lifecycle-common/src/test/java/androidx/lifecycle/observers/DerivedSequence1.kt b/lifecycle/lifecycle-common/src/jvmTest/java/androidx/lifecycle/observers/DerivedSequence1.kt
similarity index 100%
rename from lifecycle/lifecycle-common/src/test/java/androidx/lifecycle/observers/DerivedSequence1.kt
rename to lifecycle/lifecycle-common/src/jvmTest/java/androidx/lifecycle/observers/DerivedSequence1.kt
diff --git a/lifecycle/lifecycle-common/src/test/java/androidx/lifecycle/observers/DerivedSequence2.kt b/lifecycle/lifecycle-common/src/jvmTest/java/androidx/lifecycle/observers/DerivedSequence2.kt
similarity index 100%
rename from lifecycle/lifecycle-common/src/test/java/androidx/lifecycle/observers/DerivedSequence2.kt
rename to lifecycle/lifecycle-common/src/jvmTest/java/androidx/lifecycle/observers/DerivedSequence2.kt
diff --git a/lifecycle/lifecycle-common/src/test/java/androidx/lifecycle/observers/DerivedWithNewMethods.kt b/lifecycle/lifecycle-common/src/jvmTest/java/androidx/lifecycle/observers/DerivedWithNewMethods.kt
similarity index 100%
rename from lifecycle/lifecycle-common/src/test/java/androidx/lifecycle/observers/DerivedWithNewMethods.kt
rename to lifecycle/lifecycle-common/src/jvmTest/java/androidx/lifecycle/observers/DerivedWithNewMethods.kt
diff --git a/lifecycle/lifecycle-common/src/test/java/androidx/lifecycle/observers/DerivedWithNoNewMethods.kt b/lifecycle/lifecycle-common/src/jvmTest/java/androidx/lifecycle/observers/DerivedWithNoNewMethods.kt
similarity index 100%
rename from lifecycle/lifecycle-common/src/test/java/androidx/lifecycle/observers/DerivedWithNoNewMethods.kt
rename to lifecycle/lifecycle-common/src/jvmTest/java/androidx/lifecycle/observers/DerivedWithNoNewMethods.kt
diff --git a/lifecycle/lifecycle-common/src/test/java/androidx/lifecycle/observers/DerivedWithOverriddenMethodsWithLfAnnotation.kt b/lifecycle/lifecycle-common/src/jvmTest/java/androidx/lifecycle/observers/DerivedWithOverriddenMethodsWithLfAnnotation.kt
similarity index 100%
rename from lifecycle/lifecycle-common/src/test/java/androidx/lifecycle/observers/DerivedWithOverriddenMethodsWithLfAnnotation.kt
rename to lifecycle/lifecycle-common/src/jvmTest/java/androidx/lifecycle/observers/DerivedWithOverriddenMethodsWithLfAnnotation.kt
diff --git a/lifecycle/lifecycle-common/src/test/java/androidx/lifecycle/observers/Interface1.kt b/lifecycle/lifecycle-common/src/jvmTest/java/androidx/lifecycle/observers/Interface1.kt
similarity index 100%
rename from lifecycle/lifecycle-common/src/test/java/androidx/lifecycle/observers/Interface1.kt
rename to lifecycle/lifecycle-common/src/jvmTest/java/androidx/lifecycle/observers/Interface1.kt
diff --git a/lifecycle/lifecycle-common/src/test/java/androidx/lifecycle/observers/Interface1_LifecycleAdapter.kt b/lifecycle/lifecycle-common/src/jvmTest/java/androidx/lifecycle/observers/Interface1_LifecycleAdapter.kt
similarity index 100%
rename from lifecycle/lifecycle-common/src/test/java/androidx/lifecycle/observers/Interface1_LifecycleAdapter.kt
rename to lifecycle/lifecycle-common/src/jvmTest/java/androidx/lifecycle/observers/Interface1_LifecycleAdapter.kt
diff --git a/lifecycle/lifecycle-common/src/test/java/androidx/lifecycle/observers/Interface2.kt b/lifecycle/lifecycle-common/src/jvmTest/java/androidx/lifecycle/observers/Interface2.kt
similarity index 100%
rename from lifecycle/lifecycle-common/src/test/java/androidx/lifecycle/observers/Interface2.kt
rename to lifecycle/lifecycle-common/src/jvmTest/java/androidx/lifecycle/observers/Interface2.kt
diff --git a/lifecycle/lifecycle-common/src/test/java/androidx/lifecycle/observers/Interface2_LifecycleAdapter.kt b/lifecycle/lifecycle-common/src/jvmTest/java/androidx/lifecycle/observers/Interface2_LifecycleAdapter.kt
similarity index 100%
rename from lifecycle/lifecycle-common/src/test/java/androidx/lifecycle/observers/Interface2_LifecycleAdapter.kt
rename to lifecycle/lifecycle-common/src/jvmTest/java/androidx/lifecycle/observers/Interface2_LifecycleAdapter.kt
diff --git a/lifecycle/lifecycle-common/src/test/java/androidx/lifecycle/observers/InterfaceImpl1.kt b/lifecycle/lifecycle-common/src/jvmTest/java/androidx/lifecycle/observers/InterfaceImpl1.kt
similarity index 100%
rename from lifecycle/lifecycle-common/src/test/java/androidx/lifecycle/observers/InterfaceImpl1.kt
rename to lifecycle/lifecycle-common/src/jvmTest/java/androidx/lifecycle/observers/InterfaceImpl1.kt
diff --git a/lifecycle/lifecycle-common/src/test/java/androidx/lifecycle/observers/InterfaceImpl2.kt b/lifecycle/lifecycle-common/src/jvmTest/java/androidx/lifecycle/observers/InterfaceImpl2.kt
similarity index 100%
rename from lifecycle/lifecycle-common/src/test/java/androidx/lifecycle/observers/InterfaceImpl2.kt
rename to lifecycle/lifecycle-common/src/jvmTest/java/androidx/lifecycle/observers/InterfaceImpl2.kt
diff --git a/lifecycle/lifecycle-common/src/test/java/androidx/lifecycle/observers/InterfaceImpl3.kt b/lifecycle/lifecycle-common/src/jvmTest/java/androidx/lifecycle/observers/InterfaceImpl3.kt
similarity index 100%
rename from lifecycle/lifecycle-common/src/test/java/androidx/lifecycle/observers/InterfaceImpl3.kt
rename to lifecycle/lifecycle-common/src/jvmTest/java/androidx/lifecycle/observers/InterfaceImpl3.kt
diff --git a/lifecycle/lifecycle-common/src/test/java/androidx/lifecycle/observers/NoOpLifecycle.kt b/lifecycle/lifecycle-common/src/jvmTest/java/androidx/lifecycle/observers/NoOpLifecycle.kt
similarity index 100%
rename from lifecycle/lifecycle-common/src/test/java/androidx/lifecycle/observers/NoOpLifecycle.kt
rename to lifecycle/lifecycle-common/src/jvmTest/java/androidx/lifecycle/observers/NoOpLifecycle.kt
diff --git a/lifecycle/lifecycle-common/src/nativeMain/kotlin/androidx/lifecycle/Lifecycle.native.kt b/lifecycle/lifecycle-common/src/nativeMain/kotlin/androidx/lifecycle/Lifecycle.native.kt
new file mode 100644
index 0000000..4798c50
--- /dev/null
+++ b/lifecycle/lifecycle-common/src/nativeMain/kotlin/androidx/lifecycle/Lifecycle.native.kt
@@ -0,0 +1,31 @@
+/*
+ * Copyright 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package androidx.lifecycle
+
+import kotlinx.atomicfu.atomic
+import kotlinx.coroutines.CoroutineScope
+
+public actual class AtomicReference<V> actual constructor(value: V) {
+    private val delegate = atomic(value)
+    public actual fun get() = delegate.value
+    public actual fun compareAndSet(expect: V, newValue: V) =
+        delegate.compareAndSet(expect, newValue)
+}
+
+public actual abstract class LifecycleCoroutineScope internal actual constructor() :
+    CoroutineScope {
+    internal actual abstract val lifecycle: Lifecycle
+}
diff --git a/lifecycle/lifecycle-common/src/nativeMain/kotlin/androidx/lifecycle/Lifecycling.native.kt b/lifecycle/lifecycle-common/src/nativeMain/kotlin/androidx/lifecycle/Lifecycling.native.kt
new file mode 100644
index 0000000..dc7ab57
--- /dev/null
+++ b/lifecycle/lifecycle-common/src/nativeMain/kotlin/androidx/lifecycle/Lifecycling.native.kt
@@ -0,0 +1,49 @@
+/*
+ * Copyright 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package androidx.lifecycle
+
+import androidx.annotation.RestrictTo
+
+/**
+ * Internal class to handle lifecycle conversion etc.
+ */
+@RestrictTo(RestrictTo.Scope.LIBRARY_GROUP_PREFIX)
+public actual object Lifecycling {
+    public actual fun lifecycleEventObserver(`object`: Any): LifecycleEventObserver {
+        val isLifecycleEventObserver = `object` is LifecycleEventObserver
+        val isDefaultLifecycleObserver = `object` is DefaultLifecycleObserver
+        if (isLifecycleEventObserver && isDefaultLifecycleObserver) {
+            return DefaultLifecycleObserverAdapter(
+                `object` as DefaultLifecycleObserver,
+                `object` as LifecycleEventObserver
+            )
+        }
+        if (isDefaultLifecycleObserver) {
+            return DefaultLifecycleObserverAdapter(`object` as DefaultLifecycleObserver, null)
+        }
+        if (isLifecycleEventObserver) {
+            return `object` as LifecycleEventObserver
+        }
+        throw IllegalArgumentException()
+    }
+
+    /**
+     * Create a name for an adapter class.
+     */
+    public actual fun getAdapterName(className: String): String {
+        return className.replace(".", "_") + "_LifecycleAdapter"
+    }
+}
diff --git a/lifecycle/lifecycle-livedata-core-ktx-lint/src/main/java/androidx/lifecycle/lint/NonNullableMutableLiveDataDetector.kt b/lifecycle/lifecycle-livedata-core-ktx-lint/src/main/java/androidx/lifecycle/lint/NonNullableMutableLiveDataDetector.kt
index a7a8099..ad4d5df 100644
--- a/lifecycle/lifecycle-livedata-core-ktx-lint/src/main/java/androidx/lifecycle/lint/NonNullableMutableLiveDataDetector.kt
+++ b/lifecycle/lifecycle-livedata-core-ktx-lint/src/main/java/androidx/lifecycle/lint/NonNullableMutableLiveDataDetector.kt
@@ -41,11 +41,11 @@
 import org.jetbrains.uast.UCallExpression
 import org.jetbrains.uast.UClass
 import org.jetbrains.uast.UElement
+import org.jetbrains.uast.UField
 import org.jetbrains.uast.UReferenceExpression
+import org.jetbrains.uast.USimpleNameReferenceExpression
 import org.jetbrains.uast.getUastParentOfType
 import org.jetbrains.uast.isNullLiteral
-import org.jetbrains.uast.kotlin.KotlinUField
-import org.jetbrains.uast.kotlin.KotlinUSimpleReferenceExpression
 import org.jetbrains.uast.resolveToUElement
 import org.jetbrains.uast.toUElement
 
@@ -81,24 +81,21 @@
 
     val methods = listOf("setValue", "postValue")
 
-    override fun getApplicableUastTypes(): List<Class<out UElement>>? {
-        return listOf(UCallExpression::class.java, UClass::class.java)
+    override fun getApplicableUastTypes(): List<Class<out UElement>> {
+        return listOf(UCallExpression::class.java, UField::class.java)
     }
 
-    override fun createUastHandler(context: JavaContext): UElementHandler? {
+    override fun createUastHandler(context: JavaContext): UElementHandler {
         return object : UElementHandler() {
-            override fun visitClass(node: UClass) {
-                for (element in node.uastDeclarations) {
-                    if (element is KotlinUField) {
-                        getFieldTypeReference(element)?.let {
-                            // map the variable name to the type reference of its expression.
-                            typesMap.put(element.name, it)
-                        }
-                    }
+            override fun visitField(node: UField) {
+                if (!isKotlin(node.lang)) return
+                getFieldTypeReference(node)?.let {
+                    // map the variable name to the type reference of its expression.
+                    typesMap.put(node.name, it)
                 }
             }
 
-            private fun getFieldTypeReference(element: KotlinUField): KtTypeReference? {
+            private fun getFieldTypeReference(element: UField): KtTypeReference? {
                 // If field has type reference, we need to use type reference
                 // Given the field `val liveDataField: MutableLiveData<Boolean> = MutableLiveData()`
                 // reference: `MutableLiveData<Boolean>`
@@ -132,7 +129,7 @@
                 var liveDataType =
                     if (receiverType != null && receiverType.hasParameters()) {
                         val receiver =
-                            (node.receiver as? KotlinUSimpleReferenceExpression)?.resolve()
+                            (node.receiver as? USimpleNameReferenceExpression)?.resolve()
                         val variable = (receiver as? PsiVariable)
                         val assignment = variable?.let {
                             UastLintUtils.findLastAssignment(it, node)
diff --git a/lifecycle/lifecycle-runtime-ktx-lint/src/main/java/androidx/lifecycle/lint/LifecycleWhenChecks.kt b/lifecycle/lifecycle-runtime-ktx-lint/src/main/java/androidx/lifecycle/lint/LifecycleWhenChecks.kt
index 167a9f3..a75e2e6 100644
--- a/lifecycle/lifecycle-runtime-ktx-lint/src/main/java/androidx/lifecycle/lint/LifecycleWhenChecks.kt
+++ b/lifecycle/lifecycle-runtime-ktx-lint/src/main/java/androidx/lifecycle/lint/LifecycleWhenChecks.kt
@@ -44,8 +44,8 @@
 import org.jetbrains.uast.ULambdaExpression
 import org.jetbrains.uast.UMethod
 import org.jetbrains.uast.USwitchClauseExpression
+import org.jetbrains.uast.USwitchClauseExpressionWithBody
 import org.jetbrains.uast.UTryExpression
-import org.jetbrains.uast.kotlin.KotlinUSwitchEntry
 import org.jetbrains.uast.toUElement
 import org.jetbrains.uast.tryResolve
 import org.jetbrains.uast.visitor.AbstractUastVisitor
@@ -200,7 +200,7 @@
             if (method.isLifecycleIsAtLeastMethod(context)) {
                 // If the case containing the lifecycle check evaluates to true, check the body
                 withNewState(checkUIAccess = false) {
-                    (node as? KotlinUSwitchEntry)?.body?.expressions?.forEach {
+                    (node as? USwitchClauseExpressionWithBody)?.body?.expressions?.forEach {
                         it.accept(this)
                     }
                 }
diff --git a/lint-checks/integration-tests/build.gradle b/lint-checks/integration-tests/build.gradle
index 408efad..4028590 100644
--- a/lint-checks/integration-tests/build.gradle
+++ b/lint-checks/integration-tests/build.gradle
@@ -32,7 +32,6 @@
 dependencies {
     implementation(project(":annotation:annotation"))
     implementation("androidx.annotation:annotation-experimental:1.4.0")
-    implementation(project(":annotation:annotation-replacewith"))
     implementation(libs.kotlinStdlib)
 }
 
diff --git a/annotation/annotation-replacewith-lint/integration-tests/src/main/java/sample/ConstructorNonStaticClass.java b/lint-checks/integration-tests/src/main/java/replacewith/ConstructorNonStaticClass.java
similarity index 97%
rename from annotation/annotation-replacewith-lint/integration-tests/src/main/java/sample/ConstructorNonStaticClass.java
rename to lint-checks/integration-tests/src/main/java/replacewith/ConstructorNonStaticClass.java
index 88368c4..312e982 100644
--- a/annotation/annotation-replacewith-lint/integration-tests/src/main/java/sample/ConstructorNonStaticClass.java
+++ b/lint-checks/integration-tests/src/main/java/replacewith/ConstructorNonStaticClass.java
@@ -14,7 +14,7 @@
  * limitations under the License.
  */
 
-package sample;
+package replacewith;
 
 /**
  * Usage of a static class constructor.
diff --git a/annotation/annotation-replacewith-lint/integration-tests/src/main/java/sample/ConstructorStaticClass.java b/lint-checks/integration-tests/src/main/java/replacewith/ConstructorStaticClass.java
similarity index 97%
rename from annotation/annotation-replacewith-lint/integration-tests/src/main/java/sample/ConstructorStaticClass.java
rename to lint-checks/integration-tests/src/main/java/replacewith/ConstructorStaticClass.java
index d663e67..9233153 100644
--- a/annotation/annotation-replacewith-lint/integration-tests/src/main/java/sample/ConstructorStaticClass.java
+++ b/lint-checks/integration-tests/src/main/java/replacewith/ConstructorStaticClass.java
@@ -14,7 +14,7 @@
  * limitations under the License.
  */
 
-package sample;
+package replacewith;
 
 /**
  * Usage of a static class constructor.
diff --git a/annotation/annotation-replacewith-lint/integration-tests/src/main/java/sample/ConstructorToStaticMethod.java b/lint-checks/integration-tests/src/main/java/replacewith/ConstructorToStaticMethod.java
similarity index 97%
rename from annotation/annotation-replacewith-lint/integration-tests/src/main/java/sample/ConstructorToStaticMethod.java
rename to lint-checks/integration-tests/src/main/java/replacewith/ConstructorToStaticMethod.java
index fb62a31..248fd0e 100644
--- a/annotation/annotation-replacewith-lint/integration-tests/src/main/java/sample/ConstructorToStaticMethod.java
+++ b/lint-checks/integration-tests/src/main/java/replacewith/ConstructorToStaticMethod.java
@@ -14,7 +14,7 @@
  * limitations under the License.
  */
 
-package sample;
+package replacewith;
 
 /**
  * Usage of a static class constructor.
diff --git a/annotation/annotation-replacewith-lint/integration-tests/src/main/java/sample/MethodExplicitThis.java b/lint-checks/integration-tests/src/main/java/replacewith/MethodExplicitThis.java
similarity index 97%
rename from annotation/annotation-replacewith-lint/integration-tests/src/main/java/sample/MethodExplicitThis.java
rename to lint-checks/integration-tests/src/main/java/replacewith/MethodExplicitThis.java
index 0bab04c..d297597 100644
--- a/annotation/annotation-replacewith-lint/integration-tests/src/main/java/sample/MethodExplicitThis.java
+++ b/lint-checks/integration-tests/src/main/java/replacewith/MethodExplicitThis.java
@@ -14,7 +14,7 @@
  * limitations under the License.
  */
 
-package sample;
+package replacewith;
 
 import androidx.annotation.ReplaceWith;
 
diff --git a/annotation/annotation-replacewith-lint/integration-tests/src/main/java/sample/MethodImplicitThis.java b/lint-checks/integration-tests/src/main/java/replacewith/MethodImplicitThis.java
similarity index 97%
rename from annotation/annotation-replacewith-lint/integration-tests/src/main/java/sample/MethodImplicitThis.java
rename to lint-checks/integration-tests/src/main/java/replacewith/MethodImplicitThis.java
index 6cbc0a6..4d00f70 100644
--- a/annotation/annotation-replacewith-lint/integration-tests/src/main/java/sample/MethodImplicitThis.java
+++ b/lint-checks/integration-tests/src/main/java/replacewith/MethodImplicitThis.java
@@ -14,7 +14,7 @@
  * limitations under the License.
  */
 
-package sample;
+package replacewith;
 
 import androidx.annotation.ReplaceWith;
 
diff --git a/annotation/annotation-replacewith-lint/integration-tests/src/main/java/sample/ReplaceWithUsageJava.java b/lint-checks/integration-tests/src/main/java/replacewith/ReplaceWithUsageJava.java
similarity index 98%
rename from annotation/annotation-replacewith-lint/integration-tests/src/main/java/sample/ReplaceWithUsageJava.java
rename to lint-checks/integration-tests/src/main/java/replacewith/ReplaceWithUsageJava.java
index 80da9c9..7afdee3 100644
--- a/annotation/annotation-replacewith-lint/integration-tests/src/main/java/sample/ReplaceWithUsageJava.java
+++ b/lint-checks/integration-tests/src/main/java/replacewith/ReplaceWithUsageJava.java
@@ -14,7 +14,7 @@
  * limitations under the License.
  */
 
-package sample;
+package replacewith;
 
 import android.view.View;
 
diff --git a/annotation/annotation-replacewith-lint/integration-tests/src/main/java/sample/ReplaceWithUsageKotlin.kt b/lint-checks/integration-tests/src/main/java/replacewith/ReplaceWithUsageKotlin.kt
similarity index 98%
rename from annotation/annotation-replacewith-lint/integration-tests/src/main/java/sample/ReplaceWithUsageKotlin.kt
rename to lint-checks/integration-tests/src/main/java/replacewith/ReplaceWithUsageKotlin.kt
index 375a0e2..3ce93d5 100644
--- a/annotation/annotation-replacewith-lint/integration-tests/src/main/java/sample/ReplaceWithUsageKotlin.kt
+++ b/lint-checks/integration-tests/src/main/java/replacewith/ReplaceWithUsageKotlin.kt
@@ -16,7 +16,7 @@
 
 @file:Suppress("unused")
 
-package sample
+package replacewith
 
 import android.view.View
 
diff --git a/annotation/annotation-replacewith-lint/integration-tests/src/main/java/sample/StaticFieldExplicitClass.java b/lint-checks/integration-tests/src/main/java/replacewith/StaticFieldExplicitClass.java
similarity index 97%
rename from annotation/annotation-replacewith-lint/integration-tests/src/main/java/sample/StaticFieldExplicitClass.java
rename to lint-checks/integration-tests/src/main/java/replacewith/StaticFieldExplicitClass.java
index 34cd81f..4a6ed12 100644
--- a/annotation/annotation-replacewith-lint/integration-tests/src/main/java/sample/StaticFieldExplicitClass.java
+++ b/lint-checks/integration-tests/src/main/java/replacewith/StaticFieldExplicitClass.java
@@ -14,7 +14,7 @@
  * limitations under the License.
  */
 
-package sample;
+package replacewith;
 
 /**
  * Usage of a static method with an explicit class.
diff --git a/annotation/annotation-replacewith-lint/integration-tests/src/main/java/sample/StaticFieldImplicitClass.java b/lint-checks/integration-tests/src/main/java/replacewith/StaticFieldImplicitClass.java
similarity index 90%
rename from annotation/annotation-replacewith-lint/integration-tests/src/main/java/sample/StaticFieldImplicitClass.java
rename to lint-checks/integration-tests/src/main/java/replacewith/StaticFieldImplicitClass.java
index 5611277..ae0d6f5 100644
--- a/annotation/annotation-replacewith-lint/integration-tests/src/main/java/sample/StaticFieldImplicitClass.java
+++ b/lint-checks/integration-tests/src/main/java/replacewith/StaticFieldImplicitClass.java
@@ -14,9 +14,9 @@
  * limitations under the License.
  */
 
-package sample;
+package replacewith;
 
-import static sample.ReplaceWithUsageJava.AUTOFILL_HINT_NAME;
+import static replacewith.ReplaceWithUsageJava.AUTOFILL_HINT_NAME;
 
 /**
  * Usage of a static method with an explicit class.
diff --git a/annotation/annotation-replacewith-lint/integration-tests/src/main/java/sample/StaticMethodExplicitClass.java b/lint-checks/integration-tests/src/main/java/replacewith/StaticMethodExplicitClass.java
similarity index 97%
rename from annotation/annotation-replacewith-lint/integration-tests/src/main/java/sample/StaticMethodExplicitClass.java
rename to lint-checks/integration-tests/src/main/java/replacewith/StaticMethodExplicitClass.java
index c5e54fe..de9b8cb 100644
--- a/annotation/annotation-replacewith-lint/integration-tests/src/main/java/sample/StaticMethodExplicitClass.java
+++ b/lint-checks/integration-tests/src/main/java/replacewith/StaticMethodExplicitClass.java
@@ -14,7 +14,7 @@
  * limitations under the License.
  */
 
-package sample;
+package replacewith;
 
 /**
  * Usage of a static method with an explicit class.
diff --git a/lint-checks/src/main/java/androidx/build/lint/AndroidXIssueRegistry.kt b/lint-checks/src/main/java/androidx/build/lint/AndroidXIssueRegistry.kt
index e36df4f..c9e0da4 100644
--- a/lint-checks/src/main/java/androidx/build/lint/AndroidXIssueRegistry.kt
+++ b/lint-checks/src/main/java/androidx/build/lint/AndroidXIssueRegistry.kt
@@ -79,6 +79,7 @@
                 DeprecationMismatchDetector.ISSUE,
                 RestrictToDetector.RESTRICTED,
                 ObsoleteCompatDetector.ISSUE,
+                ReplaceWithDetector.ISSUE,
             )
         }
     }
diff --git a/lint-checks/src/main/java/androidx/build/lint/BanInappropriateExperimentalUsage.kt b/lint-checks/src/main/java/androidx/build/lint/BanInappropriateExperimentalUsage.kt
index 503243f..33b8853 100644
--- a/lint-checks/src/main/java/androidx/build/lint/BanInappropriateExperimentalUsage.kt
+++ b/lint-checks/src/main/java/androidx/build/lint/BanInappropriateExperimentalUsage.kt
@@ -35,11 +35,12 @@
 import java.io.FileNotFoundException
 import org.jetbrains.uast.UAnnotated
 import org.jetbrains.uast.UAnnotation
+import org.jetbrains.uast.UCallExpression
 import org.jetbrains.uast.UClass
 import org.jetbrains.uast.UClassLiteralExpression
 import org.jetbrains.uast.UElement
 import org.jetbrains.uast.UExpression
-import org.jetbrains.uast.kotlin.KotlinUVarargExpression
+import org.jetbrains.uast.UastCallKind
 import org.jetbrains.uast.resolveToUElement
 import org.jetbrains.uast.toUElement
 
@@ -113,11 +114,14 @@
         private fun getUElementsFromOptInMarkerClass(markerClass: UExpression): List<UElement> {
             val elements = ArrayList<UElement?>()
 
-            when (markerClass) {
-                is UClassLiteralExpression -> { // opting in to single annotation
+            when {
+                markerClass is UClassLiteralExpression -> {
+                    // opting in to single annotation
                     elements.add(markerClass.toUElement())
                 }
-                is KotlinUVarargExpression -> { // opting in to multiple annotations
+                markerClass is UCallExpression &&
+                    markerClass.kind == UastCallKind.NESTED_ARRAY_INITIALIZER -> {
+                    // opting in to multiple annotations
                     val expressions: List<UExpression> = markerClass.valueArguments
                     for (expression in expressions) {
                         val uElement = (expression as UClassLiteralExpression).toUElement()
diff --git a/lint-checks/src/main/java/androidx/build/lint/ClassVerificationFailureDetector.kt b/lint-checks/src/main/java/androidx/build/lint/ClassVerificationFailureDetector.kt
index 7fe685c..6057cc1 100644
--- a/lint-checks/src/main/java/androidx/build/lint/ClassVerificationFailureDetector.kt
+++ b/lint-checks/src/main/java/androidx/build/lint/ClassVerificationFailureDetector.kt
@@ -73,13 +73,13 @@
 import org.jetbrains.uast.UInstanceExpression
 import org.jetbrains.uast.UMethod
 import org.jetbrains.uast.UParenthesizedExpression
+import org.jetbrains.uast.UReturnExpression
 import org.jetbrains.uast.USuperExpression
 import org.jetbrains.uast.UThisExpression
 import org.jetbrains.uast.UastBinaryOperator
 import org.jetbrains.uast.getContainingUClass
 import org.jetbrains.uast.getContainingUMethod
 import org.jetbrains.uast.getParentOfType
-import org.jetbrains.uast.kotlin.KotlinUImplicitReturnExpression
 import org.jetbrains.uast.toUElement
 import org.jetbrains.uast.util.isConstructorCall
 import org.jetbrains.uast.util.isMethodCall
@@ -677,7 +677,7 @@
             if (psi == null) {
                 // If there is no psi, test for the one known case where there should be an
                 // expected type, an implicit Kotlin return.
-                if (element is KotlinUImplicitReturnExpression) {
+                if (element is UReturnExpression && isKotlin(element.lang)) {
                     return (element.getParentOfType<UMethod>())?.returnType
                 }
                 return null
diff --git a/lint-checks/src/main/java/androidx/build/lint/ObsoleteCompatDetector.kt b/lint-checks/src/main/java/androidx/build/lint/ObsoleteCompatDetector.kt
index 4aba06a..68ad1afd 100644
--- a/lint-checks/src/main/java/androidx/build/lint/ObsoleteCompatDetector.kt
+++ b/lint-checks/src/main/java/androidx/build/lint/ObsoleteCompatDetector.kt
@@ -34,6 +34,7 @@
 import com.intellij.psi.impl.source.PsiClassReferenceType
 import org.jetbrains.kotlin.idea.KotlinFileType
 import org.jetbrains.uast.UAnonymousClass
+import org.jetbrains.uast.UBlockExpression
 import org.jetbrains.uast.UCallExpression
 import org.jetbrains.uast.UClass
 import org.jetbrains.uast.UExpression
@@ -43,7 +44,6 @@
 import org.jetbrains.uast.UReturnExpression
 import org.jetbrains.uast.USimpleNameReferenceExpression
 import org.jetbrains.uast.getContainingUClass
-import org.jetbrains.uast.java.JavaUCodeBlockExpression
 import org.jetbrains.uast.skipParenthesizedExprDown
 
 class ObsoleteCompatDetector : Detector(), SourceCodeScanner {
@@ -67,7 +67,6 @@
 
 private class CompatMethodHandler(val context: JavaContext) : UElementHandler() {
 
-    @Suppress("UnstableApiUsage", "UnstableApiUsage") // UMethod.parameters
     override fun visitMethod(node: UMethod) {
         // If this method probably a compat method?
         if (!node.isMaybeJetpackUtilityMethod()) return
@@ -84,10 +83,10 @@
         if (hasDeprecated && hasReplaceWith && hasDeprecatedDoc) return
 
         // Compat methods take the wrapped class as the first parameter.
-        val firstParameter = node.parameters.firstOrNull() ?: return
+        val firstParameter = node.javaPsi.parameterList.parameters.firstOrNull() ?: return
 
         // Ensure that we're dealing with a single-line method that operates on the wrapped class.
-        val expression = (node.uastBody as? JavaUCodeBlockExpression)
+        val expression = (node.uastBody as? UBlockExpression)
             ?.expressions
             ?.singleOrNull()
             ?.unwrapReturnExpression()
diff --git a/annotation/annotation-replacewith-lint/src/main/java/androidx/annotation/replacewith/lint/ReplaceWithDetector.kt b/lint-checks/src/main/java/androidx/build/lint/ReplaceWithDetector.kt
similarity index 89%
rename from annotation/annotation-replacewith-lint/src/main/java/androidx/annotation/replacewith/lint/ReplaceWithDetector.kt
rename to lint-checks/src/main/java/androidx/build/lint/ReplaceWithDetector.kt
index ba130c81..94b55d2 100644
--- a/annotation/annotation-replacewith-lint/src/main/java/androidx/annotation/replacewith/lint/ReplaceWithDetector.kt
+++ b/lint-checks/src/main/java/androidx/build/lint/ReplaceWithDetector.kt
@@ -16,8 +16,10 @@
 
 @file:Suppress("UnstableApiUsage")
 
-package androidx.annotation.replacewith.lint
+package androidx.build.lint
 
+import com.android.tools.lint.detector.api.AnnotationInfo
+import com.android.tools.lint.detector.api.AnnotationUsageInfo
 import com.android.tools.lint.detector.api.AnnotationUsageType
 import com.android.tools.lint.detector.api.Category
 import com.android.tools.lint.detector.api.ConstantEvaluator
@@ -30,19 +32,17 @@
 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.PsiElement
 import com.intellij.psi.PsiField
 import com.intellij.psi.PsiMethod
+import com.intellij.psi.PsiNewExpression
 import com.intellij.psi.impl.source.tree.TreeElement
 import org.jetbrains.kotlin.lexer.KtTokens
 import org.jetbrains.kotlin.psi.KtLiteralStringTemplateEntry
 import org.jetbrains.kotlin.psi.psiUtil.endOffset
-import org.jetbrains.uast.UAnnotation
 import org.jetbrains.uast.UCallExpression
 import org.jetbrains.uast.UElement
 import org.jetbrains.uast.UQualifiedReferenceExpression
 import org.jetbrains.uast.USimpleNameReferenceExpression
-import org.jetbrains.uast.java.JavaConstructorUCallExpression
 
 class ReplaceWithDetector : Detector(), SourceCodeScanner {
 
@@ -53,17 +53,16 @@
 
     override fun visitAnnotationUsage(
         context: JavaContext,
-        usage: UElement,
-        type: AnnotationUsageType,
-        annotation: UAnnotation,
-        qualifiedName: String,
-        method: PsiMethod?,
-        referenced: PsiElement?,
-        annotations: List<UAnnotation>,
-        allMemberAnnotations: List<UAnnotation>,
-        allClassAnnotations: List<UAnnotation>,
-        allPackageAnnotations: List<UAnnotation>
+        element: UElement,
+        annotationInfo: AnnotationInfo,
+        usageInfo: AnnotationUsageInfo
     ) {
+        val qualifiedName = annotationInfo.qualifiedName
+        val annotation = annotationInfo.annotation
+        val referenced = usageInfo.referenced
+        val usage = usageInfo.usage
+        val type = usageInfo.type
+
         // Ignore callbacks for assignment on the original declaration of an annotated field.
         if (type == AnnotationUsageType.ASSIGNMENT_RHS && usage.uastParent == referenced) return
 
@@ -77,11 +76,11 @@
                 val includeReceiver = Regex("^\\w+\\.\\w+.*\$").matches(expression)
                 val includeArguments = Regex("^.*\\w+\\(.*\\)$").matches(expression)
 
-                if (method != null && usage is UCallExpression) {
+                if (referenced is PsiMethod && usage is UCallExpression) {
                     // Per Kotlin documentation for ReplaceWith: For function calls, the replacement
                     // expression may contain argument names of the deprecated function, which will
                     // be substituted with actual parameters used in the call being updated.
-                    val argumentNamesToActualParams = method.parameters.mapIndexed { index, param ->
+                    val argsToParams = referenced.parameters.mapIndexed { index, param ->
                         param.name to usage.getArgumentForParameter(index)?.sourcePsi?.text
                     }.associate { it }
 
@@ -91,7 +90,7 @@
                     var index = 0
                     do {
                         val matchResult = search.find(expression, index) ?: break
-                        val replacement = argumentNamesToActualParams[matchResult.value]
+                        val replacement = argsToParams[matchResult.value]
                         if (replacement != null) {
                             expression = expression.replaceRange(matchResult.range, replacement)
                             index += replacement.length
@@ -100,13 +99,13 @@
                         }
                     } while (index < expression.length)
 
-                    location = when (usage) {
-                        is JavaConstructorUCallExpression -> {
+                    location = when (val sourcePsi = usage.sourcePsi) {
+                        is PsiNewExpression -> {
                             // The expression should never specify "new", but if it specifies a
                             // receiver then we should replace the call to "new". For example, if
                             // we're replacing `new Clazz("arg")` with `ClazzCompat.create("arg")`.
                             context.getConstructorLocation(
-                                usage, includeReceiver, includeArguments
+                                usage, sourcePsi, includeReceiver, includeArguments
                             )
                         }
                         else -> {
@@ -171,14 +170,15 @@
  * `receiver` to handle trimming the `new` keyword from the start of a Java constructor call.
  */
 fun JavaContext.getConstructorLocation(
-    call: JavaConstructorUCallExpression,
+    call: UCallExpression,
+    newExpression: PsiNewExpression,
     includeNew: Boolean,
     includeArguments: Boolean
 ): Location {
     if (includeArguments) {
         call.valueArguments.lastOrNull()?.let { lastArgument ->
             val argumentsEnd = lastArgument.sourcePsi?.endOffset
-            val callEnds = call.sourcePsi.endOffset
+            val callEnds = newExpression.endOffset
             if (argumentsEnd != null && argumentsEnd > callEnds) {
                 // The call element has arguments that are outside of its own range.
                 // This typically means users are making a function call using
diff --git a/annotation/annotation-replacewith-lint/src/test/kotlin/androidx/annotation/replacewith/lint/ReplaceWithDetectorConstructorTest.kt b/lint-checks/src/test/java/androidx/build/lint/replacewith/ReplaceWithDetectorConstructorTest.kt
similarity index 71%
rename from annotation/annotation-replacewith-lint/src/test/kotlin/androidx/annotation/replacewith/lint/ReplaceWithDetectorConstructorTest.kt
rename to lint-checks/src/test/java/androidx/build/lint/replacewith/ReplaceWithDetectorConstructorTest.kt
index 013239e..c8cb288 100644
--- a/annotation/annotation-replacewith-lint/src/test/kotlin/androidx/annotation/replacewith/lint/ReplaceWithDetectorConstructorTest.kt
+++ b/lint-checks/src/test/java/androidx/build/lint/replacewith/ReplaceWithDetectorConstructorTest.kt
@@ -14,7 +14,7 @@
  * limitations under the License.
  */
 
-package androidx.annotation.replacewith.lint;
+package androidx.build.lint.replacewith;
 
 import org.junit.Test
 import org.junit.runner.RunWith
@@ -26,20 +26,20 @@
     @Test
     fun constructorStaticClass() {
         val input = arrayOf(
-                javaSample("sample.ReplaceWithUsageJava"),
-                javaSample("sample.ConstructorStaticClass")
+                javaSample("replacewith.ReplaceWithUsageJava"),
+                javaSample("replacewith.ConstructorStaticClass")
         )
 
         /* ktlint-disable max-line-length */
         val expected = """
-src/sample/ConstructorStaticClass.java:25: Information: Replacement available [ReplaceWith]
+src/replacewith/ConstructorStaticClass.java:25: Information: Replacement available [ReplaceWith]
         new ReplaceWithUsageJava("parameter");
             ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
 0 errors, 0 warnings
         """.trimIndent()
 
         val expectedFixDiffs = """
-Fix for src/sample/ConstructorStaticClass.java line 25: Replace with `StringBuffer("parameter")`:
+Fix for src/replacewith/ConstructorStaticClass.java line 25: Replace with `StringBuffer("parameter")`:
 @@ -25 +25
 -         new ReplaceWithUsageJava("parameter");
 +         new StringBuffer("parameter");
@@ -52,20 +52,20 @@
     @Test
     fun constructorNonStaticClass() {
         val input = arrayOf(
-            javaSample("sample.ReplaceWithUsageJava"),
-            javaSample("sample.ConstructorNonStaticClass")
+            javaSample("replacewith.ReplaceWithUsageJava"),
+            javaSample("replacewith.ConstructorNonStaticClass")
         )
 
         /* ktlint-disable max-line-length */
         val expected = """
-src/sample/ConstructorNonStaticClass.java:25: Information: Replacement available [ReplaceWith]
+src/replacewith/ConstructorNonStaticClass.java:25: Information: Replacement available [ReplaceWith]
         new ReplaceWithUsageJava().new InnerClass("param");
                                        ~~~~~~~~~~~~~~~~~~~
 0 errors, 0 warnings
         """.trimIndent()
 
         val expectedFixDiffs = """
-Fix for src/sample/ConstructorNonStaticClass.java line 25: Replace with `InnerClass()`:
+Fix for src/replacewith/ConstructorNonStaticClass.java line 25: Replace with `InnerClass()`:
 @@ -25 +25
 -         new ReplaceWithUsageJava().new InnerClass("param");
 +         new ReplaceWithUsageJava().new InnerClass();
@@ -78,20 +78,20 @@
     @Test
     fun constructorToStaticMethod() {
         val input = arrayOf(
-            javaSample("sample.ReplaceWithUsageJava"),
-            javaSample("sample.ConstructorToStaticMethod")
+            javaSample("replacewith.ReplaceWithUsageJava"),
+            javaSample("replacewith.ConstructorToStaticMethod")
         )
 
         /* ktlint-disable max-line-length */
         val expected = """
-src/sample/ConstructorToStaticMethod.java:25: Information: Replacement available [ReplaceWith]
+src/replacewith/ConstructorToStaticMethod.java:25: Information: Replacement available [ReplaceWith]
         new ReplaceWithUsageJava(10000);
         ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
 0 errors, 0 warnings
         """.trimIndent()
 
         val expectedFixDiffs = """
-Fix for src/sample/ConstructorToStaticMethod.java line 25: Replace with `ReplaceWithUsageJava.newInstance(10000)`:
+Fix for src/replacewith/ConstructorToStaticMethod.java line 25: Replace with `ReplaceWithUsageJava.newInstance(10000)`:
 @@ -25 +25
 -         new ReplaceWithUsageJava(10000);
 +         ReplaceWithUsageJava.newInstance(10000);
diff --git a/annotation/annotation-replacewith-lint/src/test/kotlin/androidx/annotation/replacewith/lint/ReplaceWithDetectorFieldTest.kt b/lint-checks/src/test/java/androidx/build/lint/replacewith/ReplaceWithDetectorFieldTest.kt
similarity index 75%
rename from annotation/annotation-replacewith-lint/src/test/kotlin/androidx/annotation/replacewith/lint/ReplaceWithDetectorFieldTest.kt
rename to lint-checks/src/test/java/androidx/build/lint/replacewith/ReplaceWithDetectorFieldTest.kt
index 22e983a..61474a2 100644
--- a/annotation/annotation-replacewith-lint/src/test/kotlin/androidx/annotation/replacewith/lint/ReplaceWithDetectorFieldTest.kt
+++ b/lint-checks/src/test/java/androidx/build/lint/replacewith/ReplaceWithDetectorFieldTest.kt
@@ -14,7 +14,7 @@
  * limitations under the License.
  */
 
-package androidx.annotation.replacewith.lint
+package androidx.build.lint.replacewith
 
 import com.android.tools.lint.checks.infrastructure.TestLintTask.lint
 import org.junit.Test
@@ -27,20 +27,20 @@
     @Test
     fun staticFieldExplicitClass() {
         val input = arrayOf(
-            javaSample("sample.ReplaceWithUsageJava"),
-            javaSample("sample.StaticFieldExplicitClass")
+            javaSample("replacewith.ReplaceWithUsageJava"),
+            javaSample("replacewith.StaticFieldExplicitClass")
         )
 
         /* ktlint-disable max-line-length */
         val expected = """
-src/sample/StaticFieldExplicitClass.java:25: Information: Replacement available [ReplaceWith]
+src/replacewith/StaticFieldExplicitClass.java:25: Information: Replacement available [ReplaceWith]
         System.out.println(ReplaceWithUsageJava.AUTOFILL_HINT_NAME);
                            ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
 0 errors, 0 warnings
         """.trimIndent()
 
         val expectedFixDiffs = """
-Fix for src/sample/StaticFieldExplicitClass.java line 25: Replace with `View.AUTOFILL_HINT_NAME`:
+Fix for src/replacewith/StaticFieldExplicitClass.java line 25: Replace with `View.AUTOFILL_HINT_NAME`:
 @@ -25 +25
 -         System.out.println(ReplaceWithUsageJava.AUTOFILL_HINT_NAME);
 +         System.out.println(View.AUTOFILL_HINT_NAME);
@@ -53,20 +53,20 @@
     @Test
     fun staticFieldImplicitClass() {
         val input = arrayOf(
-            javaSample("sample.ReplaceWithUsageJava"),
-            javaSample("sample.StaticFieldImplicitClass")
+            javaSample("replacewith.ReplaceWithUsageJava"),
+            javaSample("replacewith.StaticFieldImplicitClass")
         )
 
         /* ktlint-disable max-line-length */
         val expected = """
-src/sample/StaticFieldImplicitClass.java:27: Information: Replacement available [ReplaceWith]
+src/replacewith/StaticFieldImplicitClass.java:27: Information: Replacement available [ReplaceWith]
         System.out.println(AUTOFILL_HINT_NAME);
                            ~~~~~~~~~~~~~~~~~~
 0 errors, 0 warnings
         """.trimIndent()
 
         val expectedFixDiffs = """
-Fix for src/sample/StaticFieldImplicitClass.java line 27: Replace with `View.AUTOFILL_HINT_NAME`:
+Fix for src/replacewith/StaticFieldImplicitClass.java line 27: Replace with `View.AUTOFILL_HINT_NAME`:
 @@ -27 +27
 -         System.out.println(AUTOFILL_HINT_NAME);
 +         System.out.println(View.AUTOFILL_HINT_NAME);
diff --git a/annotation/annotation-replacewith-lint/src/test/kotlin/androidx/annotation/replacewith/lint/ReplaceWithDetectorMethodTest.kt b/lint-checks/src/test/java/androidx/build/lint/replacewith/ReplaceWithDetectorMethodTest.kt
similarity index 73%
rename from annotation/annotation-replacewith-lint/src/test/kotlin/androidx/annotation/replacewith/lint/ReplaceWithDetectorMethodTest.kt
rename to lint-checks/src/test/java/androidx/build/lint/replacewith/ReplaceWithDetectorMethodTest.kt
index e1b4e28..33d5845 100644
--- a/annotation/annotation-replacewith-lint/src/test/kotlin/androidx/annotation/replacewith/lint/ReplaceWithDetectorMethodTest.kt
+++ b/lint-checks/src/test/java/androidx/build/lint/replacewith/ReplaceWithDetectorMethodTest.kt
@@ -14,7 +14,7 @@
  * limitations under the License.
  */
 
-package androidx.annotation.replacewith.lint
+package androidx.build.lint.replacewith
 
 import org.junit.Test
 import org.junit.runner.RunWith
@@ -26,20 +26,20 @@
     @Test
     fun staticMethodExplicitClass() {
         val input = arrayOf(
-            javaSample("sample.ReplaceWithUsageJava"),
-            javaSample("sample.StaticMethodExplicitClass")
+            javaSample("replacewith.ReplaceWithUsageJava"),
+            javaSample("replacewith.StaticMethodExplicitClass")
         )
 
         /* ktlint-disable max-line-length */
         val expected = """
-src/sample/StaticMethodExplicitClass.java:25: Information: Replacement available [ReplaceWith]
+src/replacewith/StaticMethodExplicitClass.java:25: Information: Replacement available [ReplaceWith]
         ReplaceWithUsageJava.toString(this);
         ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
 0 errors, 0 warnings
         """.trimIndent()
 
         val expectedFixDiffs = """
-Fix for src/sample/StaticMethodExplicitClass.java line 25: Replace with `this.toString()`:
+Fix for src/replacewith/StaticMethodExplicitClass.java line 25: Replace with `this.toString()`:
 @@ -25 +25
 -         ReplaceWithUsageJava.toString(this);
 +         this.toString();
@@ -52,19 +52,19 @@
     @Test
     fun methodImplicitThis() {
         val input = arrayOf(
-            javaSample("sample.MethodImplicitThis")
+            javaSample("replacewith.MethodImplicitThis")
         )
 
         /* ktlint-disable max-line-length */
         val expected = """
-src/sample/MethodImplicitThis.java:33: Information: Replacement available [ReplaceWith]
+src/replacewith/MethodImplicitThis.java:33: Information: Replacement available [ReplaceWith]
         oldMethod(null);
         ~~~~~~~~~~~~~~~
 0 errors, 0 warnings
         """.trimIndent()
 
         val expectedFixDiffs = """
-Fix for src/sample/MethodImplicitThis.java line 33: Replace with `newMethod(null)`:
+Fix for src/replacewith/MethodImplicitThis.java line 33: Replace with `newMethod(null)`:
 @@ -33 +33
 -         oldMethod(null);
 +         newMethod(null);
@@ -77,19 +77,19 @@
     @Test
     fun methodExplicitThis() {
         val input = arrayOf(
-            javaSample("sample.MethodExplicitThis")
+            javaSample("replacewith.MethodExplicitThis")
         )
 
         /* ktlint-disable max-line-length */
         val expected = """
-src/sample/MethodExplicitThis.java:33: Information: Replacement available [ReplaceWith]
+src/replacewith/MethodExplicitThis.java:33: Information: Replacement available [ReplaceWith]
         this.oldMethod(null);
              ~~~~~~~~~~~~~~~
 0 errors, 0 warnings
         """.trimIndent()
 
         val expectedFixDiffs = """
-Fix for src/sample/MethodExplicitThis.java line 33: Replace with `newMethod(null)`:
+Fix for src/replacewith/MethodExplicitThis.java line 33: Replace with `newMethod(null)`:
 @@ -33 +33
 -         this.oldMethod(null);
 +         this.newMethod(null);
diff --git a/annotation/annotation-replacewith-lint/src/test/kotlin/androidx/annotation/replacewith/lint/TestUtils.kt b/lint-checks/src/test/java/androidx/build/lint/replacewith/TestUtils.kt
similarity index 96%
rename from annotation/annotation-replacewith-lint/src/test/kotlin/androidx/annotation/replacewith/lint/TestUtils.kt
rename to lint-checks/src/test/java/androidx/build/lint/replacewith/TestUtils.kt
index bcb1dc6..15dcffb 100644
--- a/annotation/annotation-replacewith-lint/src/test/kotlin/androidx/annotation/replacewith/lint/TestUtils.kt
+++ b/lint-checks/src/test/java/androidx/build/lint/replacewith/TestUtils.kt
@@ -14,8 +14,9 @@
  * limitations under the License.
  */
 
-package androidx.annotation.replacewith.lint
+package androidx.build.lint.replacewith
 
+import androidx.build.lint.ReplaceWithDetector
 import com.android.tools.lint.checks.infrastructure.TestFile
 import com.android.tools.lint.checks.infrastructure.TestFiles
 import com.android.tools.lint.checks.infrastructure.TestLintResult
diff --git a/lint/OWNERS b/lint/OWNERS
new file mode 100644
index 0000000..bc5d456
--- /dev/null
+++ b/lint/OWNERS
@@ -0,0 +1,4 @@
+# Bug component: 461356
+aurimas@google.com
+jeffrygaston@google.com
+juliamcclellan@google.com
diff --git a/annotation/annotation-replacewith-lint/build.gradle b/lint/lint-gradle/build.gradle
similarity index 62%
rename from annotation/annotation-replacewith-lint/build.gradle
rename to lint/lint-gradle/build.gradle
index 918162c..808e484 100644
--- a/annotation/annotation-replacewith-lint/build.gradle
+++ b/lint/lint-gradle/build.gradle
@@ -1,5 +1,5 @@
 /*
- * Copyright 2019 The Android Open Source Project
+ * Copyright (C) 2024 The Android Open Source Project
  *
  * Licensed under the Apache License, Version 2.0 (the "License");
  * you may not use this file except in compliance with the License.
@@ -14,6 +14,13 @@
  * limitations under the License.
  */
 
+/**
+ * This file was created using the `create_project.py` script located in the
+ * `<AndroidX root>/development/project-creator` directory.
+ *
+ * Please use that script when creating a new project, rather than copying an existing project and
+ * modifying its settings.
+ */
 import androidx.build.LibraryType
 
 plugins {
@@ -21,15 +28,9 @@
     id("kotlin")
 }
 
-sourceSets {
-    test.resources.srcDirs(
-            project(":annotation:annotation-replacewith-lint-integration-tests")
-                    .projectDir.toString() + "/src/main"
-    )
-}
-
 dependencies {
-    compileOnly(libs.androidLintMinApi)
+    compileOnly(libs.androidLintApi)
+    compileOnly(libs.androidLintChecks)
     compileOnly(libs.kotlinStdlib)
 
     testImplementation(libs.kotlinStdlib)
@@ -39,10 +40,9 @@
 }
 
 androidx {
-    name = "ReplaceWith annotation lint checks"
+    name = "Gradle lint checks"
     type = LibraryType.LINT
-    mavenVersion = LibraryVersions.ANNOTATION_REPLACEWITH
     inceptionYear = "2024"
-    description = "Lint checks for the ReplaceWith annotation library. Also enforces the " +
-            "semantics of Kotlin @Deprecated API replacements from within Java source code."
+    description = "Lint checks to verify usage of Gradle APIs."
 }
+
diff --git a/lint/lint-gradle/src/main/java/androidx/lint/gradle/EagerTaskConfigurationDetector.kt b/lint/lint-gradle/src/main/java/androidx/lint/gradle/EagerTaskConfigurationDetector.kt
new file mode 100644
index 0000000..b904f17
--- /dev/null
+++ b/lint/lint-gradle/src/main/java/androidx/lint/gradle/EagerTaskConfigurationDetector.kt
@@ -0,0 +1,103 @@
+/*
+ * Copyright 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.lint.gradle
+
+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.Incident
+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 org.jetbrains.uast.UCallExpression
+import org.jetbrains.uast.UElement
+
+/**
+ * Checks for usages of [eager APIs](https://docs.gradle.org/current/userguide/task_configuration_avoidance.html).
+ */
+class EagerTaskConfigurationDetector : Detector(), Detector.UastScanner {
+
+    override fun getApplicableUastTypes(): List<Class<out UElement>> = listOf(
+        UCallExpression::class.java
+    )
+
+    override fun createUastHandler(context: JavaContext): UElementHandler = object :
+        UElementHandler() {
+        override fun visitCallExpression(node: UCallExpression) {
+            val (containingClassName, replacementMethod) = REPLACEMENTS[node.methodName] ?: return
+            val method = node.resolve() ?: return
+            val containingClass = method.containingClass ?: return
+            // Check that the called method is from the expected class (or a child class) and not an
+            // unrelated method with the same name).
+            if (
+                containingClass.qualifiedName != containingClassName &&
+                    containingClass.supers.none { it.qualifiedName == containingClassName }
+            ) return
+
+            val fix = fix()
+                .replace()
+                .with(replacementMethod)
+                .reformat(true)
+                // Don't auto-fix from the command line because the replacement methods don't have
+                // the same return types, so the fixed code likely won't compile.
+                .autoFix(robot = false, independent = false)
+                .build()
+            val incident = Incident(context)
+                .issue(ISSUE)
+                .location(context.getNameLocation(node))
+                .message("Use $replacementMethod instead of ${method.name}")
+                .fix(fix)
+                .scope(node)
+            context.report(incident)
+        }
+    }
+
+    companion object {
+        private const val TASK_CONTAINER = "org.gradle.api.tasks.TaskContainer"
+        private const val DOMAIN_OBJECT_COLLECTION = "org.gradle.api.DomainObjectCollection"
+        private const val TASK_COLLECTION = "org.gradle.api.tasks.TaskCollection"
+
+        // A map from eager method name to the containing class of the method and the name of the
+        // replacement method.
+        private val REPLACEMENTS = mapOf(
+            "create" to Pair(TASK_CONTAINER, "register"),
+            "getByName" to Pair(TASK_CONTAINER, "named"),
+            "all" to Pair(DOMAIN_OBJECT_COLLECTION, "configureEach"),
+            "whenTaskAdded" to Pair(TASK_CONTAINER, "configureEach"),
+            "whenObjectAdded" to Pair(DOMAIN_OBJECT_COLLECTION, "configureEach"),
+            "getAt" to Pair(TASK_COLLECTION, "named"),
+        )
+
+        val ISSUE = Issue.create(
+            "EagerGradleTaskConfiguration",
+            "Avoid using eager task APIs",
+            """
+                Lazy APIs defer task configuration until the task is needed instead of doing
+                unnecessary work in the configuration phase.
+                See https://docs.gradle.org/current/userguide/task_configuration_avoidance.html for
+                more details.
+            """,
+            Category.CORRECTNESS, 5, Severity.ERROR,
+            Implementation(
+                EagerTaskConfigurationDetector::class.java,
+                Scope.JAVA_FILE_SCOPE
+            )
+        )
+    }
+}
diff --git a/annotation/annotation-replacewith-lint/src/main/java/androidx/annotation/replacewith/lint/ReplaceWithIssueRegistry.kt b/lint/lint-gradle/src/main/java/androidx/lint/gradle/GradleIssueRegistry.kt
similarity index 66%
rename from annotation/annotation-replacewith-lint/src/main/java/androidx/annotation/replacewith/lint/ReplaceWithIssueRegistry.kt
rename to lint/lint-gradle/src/main/java/androidx/lint/gradle/GradleIssueRegistry.kt
index da3620f..ea34e6f 100644
--- a/annotation/annotation-replacewith-lint/src/main/java/androidx/annotation/replacewith/lint/ReplaceWithIssueRegistry.kt
+++ b/lint/lint-gradle/src/main/java/androidx/lint/gradle/GradleIssueRegistry.kt
@@ -1,5 +1,5 @@
 /*
- * Copyright 2019 The Android Open Source Project
+ * Copyright 2024 The Android Open Source Project
  *
  * Licensed under the Apache License, Version 2.0 (the "License");
  * you may not use this file except in compliance with the License.
@@ -14,20 +14,26 @@
  * limitations under the License.
  */
 
-package androidx.annotation.replacewith.lint
+package androidx.lint.gradle
 
 import com.android.tools.lint.client.api.IssueRegistry
 import com.android.tools.lint.client.api.Vendor
 import com.android.tools.lint.detector.api.CURRENT_API
 
-@Suppress("UnstableApiUsage")
-class ReplaceWithIssueRegistry : IssueRegistry() {
-    override val minApi = CURRENT_API
-    override val api = 14
-    override val issues get() = listOf(ReplaceWithDetector.ISSUE)
+/**
+ * Collection of Gradle lint check issues.
+ */
+class GradleIssueRegistry : IssueRegistry() {
+    override val api = CURRENT_API
+
+    override val issues = listOf(
+        EagerTaskConfigurationDetector.ISSUE,
+    )
+
     override val vendor = Vendor(
-        feedbackUrl = "https://issuetracker.google.com/issues/new?component=459778",
-        identifier = "androidx.annotation.replacewith",
+        // TODO: Update component (or the issue template)
+        feedbackUrl = "https://issuetracker.google.com/issues/new?component=1147525",
+        identifier = "androidx.gradle-lint:gradle-lint-checks",
         vendorName = "Android Open Source Project",
     )
 }
diff --git a/lint/lint-gradle/src/main/resources/META-INF/services/com.android.tools.lint.client.api.IssueRegistry b/lint/lint-gradle/src/main/resources/META-INF/services/com.android.tools.lint.client.api.IssueRegistry
new file mode 100644
index 0000000..e362395
--- /dev/null
+++ b/lint/lint-gradle/src/main/resources/META-INF/services/com.android.tools.lint.client.api.IssueRegistry
@@ -0,0 +1 @@
+androidx.lint.gradle.GradleIssueRegistry
diff --git a/lint/lint-gradle/src/test/java/androidx/lint/gradle/EagerTaskConfigurationDetectorTest.kt b/lint/lint-gradle/src/test/java/androidx/lint/gradle/EagerTaskConfigurationDetectorTest.kt
new file mode 100644
index 0000000..d52b3d8
--- /dev/null
+++ b/lint/lint-gradle/src/test/java/androidx/lint/gradle/EagerTaskConfigurationDetectorTest.kt
@@ -0,0 +1,201 @@
+/*
+ * Copyright 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.lint.gradle
+
+import org.junit.Test
+import org.junit.runner.RunWith
+import org.junit.runners.JUnit4
+
+@RunWith(JUnit4::class)
+class EagerTaskConfigurationDetectorTest : GradleLintDetectorTest(
+    detector = EagerTaskConfigurationDetector(),
+    issues = listOf(EagerTaskConfigurationDetector.ISSUE)
+) {
+    @Test
+    fun `Test usage of TaskContainer#create`() {
+        val input = kotlin(
+            """
+                import org.gradle.api.Project
+
+                fun configure(project: Project) {
+                    project.tasks.create("example")
+                }
+            """.trimIndent()
+        )
+
+        val expected = """
+            src/test.kt:4: Error: Use register instead of create [EagerGradleTaskConfiguration]
+                project.tasks.create("example")
+                              ~~~~~~
+            1 errors, 0 warnings
+        """.trimIndent()
+        val expectedFixDiffs = """
+            Fix for src/test.kt line 4: Replace with register:
+            @@ -4 +4
+            -     project.tasks.create("example")
+            +     project.tasks.register("example")
+        """.trimIndent()
+
+        check(input).expect(expected).expectFixDiffs(expectedFixDiffs)
+    }
+
+    @Test
+    fun `Test usage of TaskContainer#getByName`() {
+        val input = kotlin(
+            """
+                import org.gradle.api.Project
+
+                fun configure(project: Project) {
+                    project.tasks.getByName("example")
+                }
+            """.trimIndent()
+        )
+
+        val expected = """
+            src/test.kt:4: Error: Use named instead of getByName [EagerGradleTaskConfiguration]
+                project.tasks.getByName("example")
+                              ~~~~~~~~~
+            1 errors, 0 warnings
+        """.trimIndent()
+        val expectedFixDiffs = """
+            Fix for src/test.kt line 4: Replace with named:
+            @@ -4 +4
+            -     project.tasks.getByName("example")
+            +     project.tasks.named("example")
+        """.trimIndent()
+
+        check(input).expect(expected).expectFixDiffs(expectedFixDiffs)
+    }
+
+    @Test
+    fun `Test usage of DomainObjectCollection#all`() {
+        val input = kotlin(
+            """
+                import org.gradle.api.Action
+                import org.gradle.api.Project
+                import org.gradle.api.Task
+
+                fun configure(project: Project, action: Action<Task>) {
+                    project.tasks.all(action)
+                }
+            """.trimIndent()
+        )
+
+        val expected = """
+            src/test.kt:6: Error: Use configureEach instead of all [EagerGradleTaskConfiguration]
+                project.tasks.all(action)
+                              ~~~
+            1 errors, 0 warnings
+        """.trimIndent()
+        val expectedFixDiffs = """
+            Fix for src/test.kt line 6: Replace with configureEach:
+            @@ -6 +6
+            -     project.tasks.all(action)
+            +     project.tasks.configureEach(action)
+        """.trimIndent()
+
+        check(input).expect(expected).expectFixDiffs(expectedFixDiffs)
+    }
+
+    @Test
+    fun `Test usage of TaskContainer#whenTaskAdded`() {
+        val input = kotlin(
+            """
+                import org.gradle.api.Action
+                import org.gradle.api.Project
+                import org.gradle.api.Task
+
+                fun configure(project: Project, action: Action<Task>) {
+                    project.tasks.whenTaskAdded(action)
+                }
+            """.trimIndent()
+        )
+
+        val expected = """
+            src/test.kt:6: Error: Use configureEach instead of whenTaskAdded [EagerGradleTaskConfiguration]
+                project.tasks.whenTaskAdded(action)
+                              ~~~~~~~~~~~~~
+            1 errors, 0 warnings
+        """.trimIndent()
+        val expectedFixDiffs = """
+            Fix for src/test.kt line 6: Replace with configureEach:
+            @@ -6 +6
+            -     project.tasks.whenTaskAdded(action)
+            +     project.tasks.configureEach(action)
+        """.trimIndent()
+
+        check(input).expect(expected).expectFixDiffs(expectedFixDiffs)
+    }
+
+    @Test
+    fun `Test usage of DomainObjectCollection#whenObjectAdded`() {
+        val input = kotlin(
+            """
+                import org.gradle.api.Action
+                import org.gradle.api.Project
+                import org.gradle.api.Task
+
+                fun configure(project: Project, action: Action<Task>) {
+                    project.tasks.whenObjectAdded(action)
+                }
+            """.trimIndent()
+        )
+
+        val expected = """
+            src/test.kt:6: Error: Use configureEach instead of whenObjectAdded [EagerGradleTaskConfiguration]
+                project.tasks.whenObjectAdded(action)
+                              ~~~~~~~~~~~~~~~
+            1 errors, 0 warnings
+        """.trimIndent()
+        val expectedFixDiffs = """
+            Fix for src/test.kt line 6: Replace with configureEach:
+            @@ -6 +6
+            -     project.tasks.whenObjectAdded(action)
+            +     project.tasks.configureEach(action)
+        """.trimIndent()
+
+        check(input).expect(expected).expectFixDiffs(expectedFixDiffs)
+    }
+
+    @Test
+    fun `Test usage of TaskCollection#getAt`() {
+        val input = kotlin(
+            """
+                import org.gradle.api.Project
+
+                fun configure(project: Project) {
+                    project.tasks.getAt("example")
+                }
+            """.trimIndent()
+        )
+
+        val expected = """
+            src/test.kt:4: Error: Use named instead of getAt [EagerGradleTaskConfiguration]
+                project.tasks.getAt("example")
+                              ~~~~~
+            1 errors, 0 warnings
+        """.trimIndent()
+        val expectedFixDiffs = """
+            Fix for src/test.kt line 4: Replace with named:
+            @@ -4 +4
+            -     project.tasks.getAt("example")
+            +     project.tasks.named("example")
+        """.trimIndent()
+
+        check(input).expect(expected).expectFixDiffs(expectedFixDiffs)
+    }
+}
diff --git a/lint/lint-gradle/src/test/java/androidx/lint/gradle/GradleLintDetectorTest.kt b/lint/lint-gradle/src/test/java/androidx/lint/gradle/GradleLintDetectorTest.kt
new file mode 100644
index 0000000..64dff60
--- /dev/null
+++ b/lint/lint-gradle/src/test/java/androidx/lint/gradle/GradleLintDetectorTest.kt
@@ -0,0 +1,37 @@
+/*
+ * Copyright 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.lint.gradle
+
+import com.android.tools.lint.checks.infrastructure.LintDetectorTest
+import com.android.tools.lint.checks.infrastructure.TestFile
+import com.android.tools.lint.checks.infrastructure.TestLintResult
+import com.android.tools.lint.detector.api.Detector
+import com.android.tools.lint.detector.api.Issue
+
+/** Base test setup for lint checks in this project, providing the defined Gradle [STUBS]. */
+abstract class GradleLintDetectorTest(
+    private val detector: Detector,
+    private val issues: List<Issue>
+) : LintDetectorTest() {
+    override fun getDetector(): Detector = detector
+    override fun getIssues(): List<Issue> = issues
+
+    /** Convenience method for running a lint test over the input [files] and [STUBS]. */
+    fun check(vararg files: TestFile): TestLintResult {
+        return lint().files(*STUBS, *files).run()
+    }
+}
diff --git a/lint/lint-gradle/src/test/java/androidx/lint/gradle/Stubs.kt b/lint/lint-gradle/src/test/java/androidx/lint/gradle/Stubs.kt
new file mode 100644
index 0000000..245e6c6
--- /dev/null
+++ b/lint/lint-gradle/src/test/java/androidx/lint/gradle/Stubs.kt
@@ -0,0 +1,68 @@
+/*
+ * Copyright 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.lint.gradle
+
+import com.android.tools.lint.checks.infrastructure.TestFiles.kotlin
+
+/**
+ * Common-use stubs of the Gradle API for use in tests. If a test requires additional definitions to
+ * run, these should be added to.
+ */
+internal val STUBS =
+    arrayOf(
+        kotlin(
+            """
+                package org.gradle.api.tasks
+
+                import org.gradle.api.DomainObjectCollection
+                import org.gradle.api.Task
+
+                class TaskContainer : DomainObjectCollection<Task>, TaskCollection<Task> {
+                    fun create(name: String) = Unit
+                    fun register(name: String) = Unit
+                    fun getByName(name: String) = Unit
+                    fun named(name: String) = Unit
+                    fun whenTaskAdded(action: Action<in T>)
+                }
+
+                interface TaskCollection<T : Task> {
+                    fun getAt(name: String) = Unit
+                }
+            """.trimIndent()
+        ),
+        kotlin(
+            """
+                package org.gradle.api
+
+                import org.gradle.api.tasks.TaskContainer
+
+                class Project {
+                    val tasks: TaskContainer
+                }
+
+                interface DomainObjectCollection<T> {
+                    fun all(action: Action<in T>)
+                    fun configureEach(action: Action<in T>)
+                    fun whenObjectAdded(action: Action<in T>)
+                }
+
+                interface Action<T>
+
+                interface Task
+            """.trimIndent()
+        )
+    )
diff --git a/navigation/navigation-common-lint/src/main/java/androidx/navigation/common/lint/EmptyNavDeepLinkDetector.kt b/navigation/navigation-common-lint/src/main/java/androidx/navigation/common/lint/EmptyNavDeepLinkDetector.kt
index 5606b24..0793ef5d 100644
--- a/navigation/navigation-common-lint/src/main/java/androidx/navigation/common/lint/EmptyNavDeepLinkDetector.kt
+++ b/navigation/navigation-common-lint/src/main/java/androidx/navigation/common/lint/EmptyNavDeepLinkDetector.kt
@@ -27,9 +27,9 @@
 import com.android.tools.lint.detector.api.Severity
 import com.android.tools.lint.detector.api.SourceCodeScanner
 import com.intellij.psi.PsiMethod
+import org.jetbrains.uast.UBlockExpression
 import org.jetbrains.uast.UCallExpression
-import org.jetbrains.uast.kotlin.KotlinUBlockExpression
-import org.jetbrains.uast.kotlin.KotlinULambdaExpression
+import org.jetbrains.uast.ULambdaExpression
 
 /**
  * Lint for checking for empty construction of NavDeepLink in the Kotlin DSL,
@@ -57,8 +57,8 @@
     override fun visitMethodCall(context: JavaContext, node: UCallExpression, method: PsiMethod) {
         // valueArgumentCount should be 1 when navDeepLink is called
         if (node.valueArgumentCount > 0) {
-            val lam = node.valueArguments[0] as KotlinULambdaExpression
-            val body = lam.body as KotlinUBlockExpression
+            val lam = node.valueArguments[0] as ULambdaExpression
+            val body = lam.body as UBlockExpression
             if (body.expressions.isEmpty()) {
                 context.report(
                     EmptyNavDeepLink,
diff --git a/navigation/navigation-safe-args-gradle-plugin/src/main/kotlin/androidx/navigation/safeargs/gradle/SafeArgsPlugin.kt b/navigation/navigation-safe-args-gradle-plugin/src/main/kotlin/androidx/navigation/safeargs/gradle/SafeArgsPlugin.kt
index e24917a..a13bd56 100644
--- a/navigation/navigation-safe-args-gradle-plugin/src/main/kotlin/androidx/navigation/safeargs/gradle/SafeArgsPlugin.kt
+++ b/navigation/navigation-safe-args-gradle-plugin/src/main/kotlin/androidx/navigation/safeargs/gradle/SafeArgsPlugin.kt
@@ -50,9 +50,9 @@
         action: (com.android.build.gradle.api.BaseVariant) -> Unit
     ) {
         when {
-            extension is AppExtension -> extension.applicationVariants.all(action)
+            extension is AppExtension -> extension.applicationVariants.configureEach(action)
             extension is LibraryExtension -> {
-                extension.libraryVariants.all(action)
+                extension.libraryVariants.configureEach(action)
             }
             else -> throw GradleException(
                 "safeargs plugin must be used with android app," +
diff --git a/paging/paging-common/api/current.txt b/paging/paging-common/api/current.txt
index 9640e44..149fe1c 100644
--- a/paging/paging-common/api/current.txt
+++ b/paging/paging-common/api/current.txt
@@ -12,13 +12,21 @@
     method public androidx.paging.LoadState getPrepend();
     method public androidx.paging.LoadState getRefresh();
     method public androidx.paging.LoadStates getSource();
+    method public boolean hasError();
+    method public boolean isIdle();
     property public final androidx.paging.LoadState append;
+    property public final boolean hasError;
+    property public final boolean isIdle;
     property public final androidx.paging.LoadStates? mediator;
     property public final androidx.paging.LoadState prepend;
     property public final androidx.paging.LoadState refresh;
     property public final androidx.paging.LoadStates source;
   }
 
+  public final class CombinedLoadStatesKt {
+    method public static suspend Object? awaitNotLoading(kotlinx.coroutines.flow.Flow<androidx.paging.CombinedLoadStates>, kotlin.coroutines.Continuation<androidx.paging.CombinedLoadStates>);
+  }
+
   public abstract class DataSource<Key, Value> {
     method @AnyThread public void addInvalidatedCallback(androidx.paging.DataSource.InvalidatedCallback onInvalidatedCallback);
     method @AnyThread public void invalidate();
@@ -131,7 +139,11 @@
     method public androidx.paging.LoadState getAppend();
     method public androidx.paging.LoadState getPrepend();
     method public androidx.paging.LoadState getRefresh();
+    method public boolean hasError();
+    method public boolean isIdle();
     property public final androidx.paging.LoadState append;
+    property public final boolean hasError;
+    property public final boolean isIdle;
     property public final androidx.paging.LoadState prepend;
     property public final androidx.paging.LoadState refresh;
   }
@@ -319,6 +331,77 @@
     method public <T> androidx.paging.PagingData<T> from(java.util.List<? extends T> data, androidx.paging.LoadStates sourceLoadStates, optional androidx.paging.LoadStates? mediatorLoadStates);
   }
 
+  public abstract sealed class PagingDataEvent<T> {
+  }
+
+  public static final class PagingDataEvent.Append<T> extends androidx.paging.PagingDataEvent<T> {
+    method public java.util.List<T> getInserted();
+    method public int getNewPlaceholdersAfter();
+    method public int getOldPlaceholdersAfter();
+    method public int getStartIndex();
+    property public final java.util.List<T> inserted;
+    property public final int newPlaceholdersAfter;
+    property public final int oldPlaceholdersAfter;
+    property public final int startIndex;
+  }
+
+  public static final class PagingDataEvent.DropAppend<T> extends androidx.paging.PagingDataEvent<T> {
+    method public int getDropCount();
+    method public int getNewPlaceholdersAfter();
+    method public int getOldPlaceholdersAfter();
+    method public int getStartIndex();
+    property public final int dropCount;
+    property public final int newPlaceholdersAfter;
+    property public final int oldPlaceholdersAfter;
+    property public final int startIndex;
+  }
+
+  public static final class PagingDataEvent.DropPrepend<T> extends androidx.paging.PagingDataEvent<T> {
+    method public int getDropCount();
+    method public int getNewPlaceholdersBefore();
+    method public int getOldPlaceholdersBefore();
+    property public final int dropCount;
+    property public final int newPlaceholdersBefore;
+    property public final int oldPlaceholdersBefore;
+  }
+
+  public static final class PagingDataEvent.Prepend<T> extends androidx.paging.PagingDataEvent<T> {
+    method public java.util.List<T> getInserted();
+    method public int getNewPlaceholdersBefore();
+    method public int getOldPlaceholdersBefore();
+    property public final java.util.List<T> inserted;
+    property public final int newPlaceholdersBefore;
+    property public final int oldPlaceholdersBefore;
+  }
+
+  public static final class PagingDataEvent.Refresh<T> extends androidx.paging.PagingDataEvent<T> {
+    method public androidx.paging.PlaceholderPaddedList<T> getNewList();
+    method public androidx.paging.PlaceholderPaddedList<T> getPreviousList();
+    property public final androidx.paging.PlaceholderPaddedList<T> newList;
+    property public final androidx.paging.PlaceholderPaddedList<T> previousList;
+  }
+
+  public abstract class PagingDataPresenter<T> {
+    ctor public PagingDataPresenter(optional kotlin.coroutines.CoroutineContext mainContext, optional androidx.paging.PagingData<T>? cachedPagingData);
+    method public final void addLoadStateListener(kotlin.jvm.functions.Function1<androidx.paging.CombinedLoadStates,kotlin.Unit> listener);
+    method public final void addOnPagesUpdatedListener(kotlin.jvm.functions.Function0<kotlin.Unit> listener);
+    method public final suspend Object? collectFrom(androidx.paging.PagingData<T> pagingData, kotlin.coroutines.Continuation<kotlin.Unit>);
+    method @MainThread public final operator T? get(@IntRange(from=0L) int index);
+    method public final kotlinx.coroutines.flow.StateFlow<androidx.paging.CombinedLoadStates?> getLoadStateFlow();
+    method public final kotlinx.coroutines.flow.Flow<kotlin.Unit> getOnPagesUpdatedFlow();
+    method public final int getSize();
+    method @MainThread public final T? peek(@IntRange(from=0L) int index);
+    method public abstract suspend Object? presentPagingDataEvent(androidx.paging.PagingDataEvent<T> event, kotlin.coroutines.Continuation<kotlin.Unit>);
+    method public final void refresh();
+    method public final void removeLoadStateListener(kotlin.jvm.functions.Function1<androidx.paging.CombinedLoadStates,kotlin.Unit> listener);
+    method public final void removeOnPagesUpdatedListener(kotlin.jvm.functions.Function0<kotlin.Unit> listener);
+    method public final void retry();
+    method public final androidx.paging.ItemSnapshotList<T> snapshot();
+    property public final kotlinx.coroutines.flow.StateFlow<androidx.paging.CombinedLoadStates?> loadStateFlow;
+    property public final kotlinx.coroutines.flow.Flow<kotlin.Unit> onPagesUpdatedFlow;
+    property public final int size;
+  }
+
   public final class PagingDataTransforms {
     method @CheckResult public static <T> androidx.paging.PagingData<T> filter(androidx.paging.PagingData<T>, java.util.concurrent.Executor executor, kotlin.jvm.functions.Function1<? super T,java.lang.Boolean> predicate);
     method @CheckResult @kotlin.jvm.JvmSynthetic public static <T> androidx.paging.PagingData<T> filter(androidx.paging.PagingData<T>, kotlin.jvm.functions.Function2<? super T,? super kotlin.coroutines.Continuation<? super java.lang.Boolean>,?> predicate);
@@ -438,6 +521,18 @@
     property public final java.util.List<androidx.paging.PagingSource.LoadResult.Page<Key,Value>> pages;
   }
 
+  public interface PlaceholderPaddedList<T> {
+    method public int getDataCount();
+    method public T getItem(int index);
+    method public int getPlaceholdersAfter();
+    method public int getPlaceholdersBefore();
+    method public int getSize();
+    property public abstract int dataCount;
+    property public abstract int placeholdersAfter;
+    property public abstract int placeholdersBefore;
+    property public abstract int size;
+  }
+
   @Deprecated public abstract class PositionalDataSource<T> extends androidx.paging.DataSource<java.lang.Integer,T> {
     ctor @Deprecated public PositionalDataSource();
     method @Deprecated public static final int computeInitialLoadPosition(androidx.paging.PositionalDataSource.LoadInitialParams params, int totalCount);
diff --git a/paging/paging-common/api/restricted_current.txt b/paging/paging-common/api/restricted_current.txt
index 9640e44..149fe1c 100644
--- a/paging/paging-common/api/restricted_current.txt
+++ b/paging/paging-common/api/restricted_current.txt
@@ -12,13 +12,21 @@
     method public androidx.paging.LoadState getPrepend();
     method public androidx.paging.LoadState getRefresh();
     method public androidx.paging.LoadStates getSource();
+    method public boolean hasError();
+    method public boolean isIdle();
     property public final androidx.paging.LoadState append;
+    property public final boolean hasError;
+    property public final boolean isIdle;
     property public final androidx.paging.LoadStates? mediator;
     property public final androidx.paging.LoadState prepend;
     property public final androidx.paging.LoadState refresh;
     property public final androidx.paging.LoadStates source;
   }
 
+  public final class CombinedLoadStatesKt {
+    method public static suspend Object? awaitNotLoading(kotlinx.coroutines.flow.Flow<androidx.paging.CombinedLoadStates>, kotlin.coroutines.Continuation<androidx.paging.CombinedLoadStates>);
+  }
+
   public abstract class DataSource<Key, Value> {
     method @AnyThread public void addInvalidatedCallback(androidx.paging.DataSource.InvalidatedCallback onInvalidatedCallback);
     method @AnyThread public void invalidate();
@@ -131,7 +139,11 @@
     method public androidx.paging.LoadState getAppend();
     method public androidx.paging.LoadState getPrepend();
     method public androidx.paging.LoadState getRefresh();
+    method public boolean hasError();
+    method public boolean isIdle();
     property public final androidx.paging.LoadState append;
+    property public final boolean hasError;
+    property public final boolean isIdle;
     property public final androidx.paging.LoadState prepend;
     property public final androidx.paging.LoadState refresh;
   }
@@ -319,6 +331,77 @@
     method public <T> androidx.paging.PagingData<T> from(java.util.List<? extends T> data, androidx.paging.LoadStates sourceLoadStates, optional androidx.paging.LoadStates? mediatorLoadStates);
   }
 
+  public abstract sealed class PagingDataEvent<T> {
+  }
+
+  public static final class PagingDataEvent.Append<T> extends androidx.paging.PagingDataEvent<T> {
+    method public java.util.List<T> getInserted();
+    method public int getNewPlaceholdersAfter();
+    method public int getOldPlaceholdersAfter();
+    method public int getStartIndex();
+    property public final java.util.List<T> inserted;
+    property public final int newPlaceholdersAfter;
+    property public final int oldPlaceholdersAfter;
+    property public final int startIndex;
+  }
+
+  public static final class PagingDataEvent.DropAppend<T> extends androidx.paging.PagingDataEvent<T> {
+    method public int getDropCount();
+    method public int getNewPlaceholdersAfter();
+    method public int getOldPlaceholdersAfter();
+    method public int getStartIndex();
+    property public final int dropCount;
+    property public final int newPlaceholdersAfter;
+    property public final int oldPlaceholdersAfter;
+    property public final int startIndex;
+  }
+
+  public static final class PagingDataEvent.DropPrepend<T> extends androidx.paging.PagingDataEvent<T> {
+    method public int getDropCount();
+    method public int getNewPlaceholdersBefore();
+    method public int getOldPlaceholdersBefore();
+    property public final int dropCount;
+    property public final int newPlaceholdersBefore;
+    property public final int oldPlaceholdersBefore;
+  }
+
+  public static final class PagingDataEvent.Prepend<T> extends androidx.paging.PagingDataEvent<T> {
+    method public java.util.List<T> getInserted();
+    method public int getNewPlaceholdersBefore();
+    method public int getOldPlaceholdersBefore();
+    property public final java.util.List<T> inserted;
+    property public final int newPlaceholdersBefore;
+    property public final int oldPlaceholdersBefore;
+  }
+
+  public static final class PagingDataEvent.Refresh<T> extends androidx.paging.PagingDataEvent<T> {
+    method public androidx.paging.PlaceholderPaddedList<T> getNewList();
+    method public androidx.paging.PlaceholderPaddedList<T> getPreviousList();
+    property public final androidx.paging.PlaceholderPaddedList<T> newList;
+    property public final androidx.paging.PlaceholderPaddedList<T> previousList;
+  }
+
+  public abstract class PagingDataPresenter<T> {
+    ctor public PagingDataPresenter(optional kotlin.coroutines.CoroutineContext mainContext, optional androidx.paging.PagingData<T>? cachedPagingData);
+    method public final void addLoadStateListener(kotlin.jvm.functions.Function1<androidx.paging.CombinedLoadStates,kotlin.Unit> listener);
+    method public final void addOnPagesUpdatedListener(kotlin.jvm.functions.Function0<kotlin.Unit> listener);
+    method public final suspend Object? collectFrom(androidx.paging.PagingData<T> pagingData, kotlin.coroutines.Continuation<kotlin.Unit>);
+    method @MainThread public final operator T? get(@IntRange(from=0L) int index);
+    method public final kotlinx.coroutines.flow.StateFlow<androidx.paging.CombinedLoadStates?> getLoadStateFlow();
+    method public final kotlinx.coroutines.flow.Flow<kotlin.Unit> getOnPagesUpdatedFlow();
+    method public final int getSize();
+    method @MainThread public final T? peek(@IntRange(from=0L) int index);
+    method public abstract suspend Object? presentPagingDataEvent(androidx.paging.PagingDataEvent<T> event, kotlin.coroutines.Continuation<kotlin.Unit>);
+    method public final void refresh();
+    method public final void removeLoadStateListener(kotlin.jvm.functions.Function1<androidx.paging.CombinedLoadStates,kotlin.Unit> listener);
+    method public final void removeOnPagesUpdatedListener(kotlin.jvm.functions.Function0<kotlin.Unit> listener);
+    method public final void retry();
+    method public final androidx.paging.ItemSnapshotList<T> snapshot();
+    property public final kotlinx.coroutines.flow.StateFlow<androidx.paging.CombinedLoadStates?> loadStateFlow;
+    property public final kotlinx.coroutines.flow.Flow<kotlin.Unit> onPagesUpdatedFlow;
+    property public final int size;
+  }
+
   public final class PagingDataTransforms {
     method @CheckResult public static <T> androidx.paging.PagingData<T> filter(androidx.paging.PagingData<T>, java.util.concurrent.Executor executor, kotlin.jvm.functions.Function1<? super T,java.lang.Boolean> predicate);
     method @CheckResult @kotlin.jvm.JvmSynthetic public static <T> androidx.paging.PagingData<T> filter(androidx.paging.PagingData<T>, kotlin.jvm.functions.Function2<? super T,? super kotlin.coroutines.Continuation<? super java.lang.Boolean>,?> predicate);
@@ -438,6 +521,18 @@
     property public final java.util.List<androidx.paging.PagingSource.LoadResult.Page<Key,Value>> pages;
   }
 
+  public interface PlaceholderPaddedList<T> {
+    method public int getDataCount();
+    method public T getItem(int index);
+    method public int getPlaceholdersAfter();
+    method public int getPlaceholdersBefore();
+    method public int getSize();
+    property public abstract int dataCount;
+    property public abstract int placeholdersAfter;
+    property public abstract int placeholdersBefore;
+    property public abstract int size;
+  }
+
   @Deprecated public abstract class PositionalDataSource<T> extends androidx.paging.DataSource<java.lang.Integer,T> {
     ctor @Deprecated public PositionalDataSource();
     method @Deprecated public static final int computeInitialLoadPosition(androidx.paging.PositionalDataSource.LoadInitialParams params, int totalCount);
diff --git a/paging/paging-common/src/commonJvmAndroidMain/kotlin/androidx/paging/PagedList.kt b/paging/paging-common/src/commonJvmAndroidMain/kotlin/androidx/paging/PagedList.kt
index c78f513..a9b5655 100644
--- a/paging/paging-common/src/commonJvmAndroidMain/kotlin/androidx/paging/PagedList.kt
+++ b/paging/paging-common/src/commonJvmAndroidMain/kotlin/androidx/paging/PagedList.kt
@@ -932,7 +932,7 @@
     }
 
     @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP) // protected otherwise
-    public fun getNullPaddedList(): NullPaddedList<T> = storage
+    public fun getPlaceholderPaddedList(): PlaceholderPaddedList<T> = storage
 
     internal var refreshRetryCallback: Runnable? = null
 
diff --git a/paging/paging-common/src/commonJvmAndroidMain/kotlin/androidx/paging/PagedStorage.jvm.kt b/paging/paging-common/src/commonJvmAndroidMain/kotlin/androidx/paging/PagedStorage.jvm.kt
index b727765..c6d537b 100644
--- a/paging/paging-common/src/commonJvmAndroidMain/kotlin/androidx/paging/PagedStorage.jvm.kt
+++ b/paging/paging-common/src/commonJvmAndroidMain/kotlin/androidx/paging/PagedStorage.jvm.kt
@@ -29,7 +29,7 @@
 internal class PagedStorage<T : Any> :
     AbstractList<T>,
     LegacyPageFetcher.KeyProvider<Any>,
-    NullPaddedList<T> {
+    PlaceholderPaddedList<T> {
     private val pages = mutableListOf<Page<*, T>>()
 
     internal val firstLoadedItem: T
diff --git a/paging/paging-common/src/commonMain/kotlin/androidx/paging/CombinedLoadStates.kt b/paging/paging-common/src/commonMain/kotlin/androidx/paging/CombinedLoadStates.kt
index 43bec5a2..7cb35cd 100644
--- a/paging/paging-common/src/commonMain/kotlin/androidx/paging/CombinedLoadStates.kt
+++ b/paging/paging-common/src/commonMain/kotlin/androidx/paging/CombinedLoadStates.kt
@@ -16,6 +16,15 @@
 
 package androidx.paging
 
+import androidx.paging.LoadState.NotLoading
+import kotlin.jvm.JvmName
+import kotlin.jvm.JvmSuppressWildcards
+import kotlinx.coroutines.FlowPreview
+import kotlinx.coroutines.flow.Flow
+import kotlinx.coroutines.flow.debounce
+import kotlinx.coroutines.flow.filter
+import kotlinx.coroutines.flow.firstOrNull
+
 /**
  * Collection of pagination [LoadState]s for both a [PagingSource], and [RemoteMediator].
  *
@@ -105,4 +114,47 @@
             op(type, true, state)
         }
     }
+
+    /**
+     * Returns true when [source] and [mediator] is in [NotLoading] for all [LoadType]
+     */
+    public val isIdle = source.isIdle && mediator?.isIdle ?: true
+
+    /**
+     * Returns true if either [source] or [mediator] has a [LoadType] that is in [LoadState.Error]
+     */
+    @get:JvmName("hasError")
+    public val hasError = source.hasError || mediator?.hasError ?: false
+}
+
+/**
+ * Function to wait on a Flow<CombinedLoadStates> until a load has completed.
+ *
+ * It collects on the Flow<CombinedLoadStates> and suspends until it collects and returns the
+ * firstOrNull [CombinedLoadStates] where all [LoadStates] have settled into a non-loading state
+ * i.e. [LoadState.NotLoading] or [LoadState.Error].
+ *
+ * A use case could be scrolling to a position after refresh has completed:
+ * ```
+ * override fun onCreate(savedInstanceState: Bundle?) {
+ *     ...
+ *     refreshButton.setOnClickListener {
+ *         pagingAdapter.refresh()
+ *         lifecycleScope.launch {
+ *             // wait for refresh to complete
+ *             pagingAdapter.loadStateFlow.awaitNotLoading()
+ *             // do work after refresh
+ *             recyclerView.scrollToPosition(position)
+ *         }
+ *    }
+ * }
+ * ```
+ */
+@OptIn(FlowPreview::class)
+public suspend fun Flow<CombinedLoadStates>.awaitNotLoading():
+    @JvmSuppressWildcards CombinedLoadStates? {
+
+    return debounce(1).filter {
+        it.isIdle || it.hasError
+    }.firstOrNull()
 }
diff --git a/paging/paging-common/src/commonMain/kotlin/androidx/paging/LoadStates.kt b/paging/paging-common/src/commonMain/kotlin/androidx/paging/LoadStates.kt
index 442148f..c0626db 100644
--- a/paging/paging-common/src/commonMain/kotlin/androidx/paging/LoadStates.kt
+++ b/paging/paging-common/src/commonMain/kotlin/androidx/paging/LoadStates.kt
@@ -18,6 +18,7 @@
 
 import androidx.annotation.RestrictTo
 import androidx.paging.LoadState.NotLoading
+import kotlin.jvm.JvmName
 
 /**
  * Collection of pagination [LoadState]s - refresh, prepend, and append.
@@ -57,6 +58,20 @@
         LoadType.PREPEND -> prepend
     }
 
+    /**
+     * Returns true if either one of [refresh], [append], or [prepend] is in [Error] state.
+     */
+    @get:JvmName("hasError")
+    public val hasError = refresh is LoadState.Error || append is LoadState.Error ||
+        prepend is LoadState.Error
+
+    /**
+     * Returns true if all three LoadState [refresh], [append], and [prepend] are
+     * in [NotLoading] state.
+     */
+    public val isIdle = refresh is NotLoading && append is NotLoading &&
+        prepend is NotLoading
+
     internal companion object {
         val IDLE = LoadStates(
             refresh = NotLoading.Incomplete,
diff --git a/paging/paging-common/src/commonMain/kotlin/androidx/paging/PageStore.kt b/paging/paging-common/src/commonMain/kotlin/androidx/paging/PageStore.kt
index 3167150..500e6e5 100644
--- a/paging/paging-common/src/commonMain/kotlin/androidx/paging/PageStore.kt
+++ b/paging/paging-common/src/commonMain/kotlin/androidx/paging/PageStore.kt
@@ -30,7 +30,7 @@
     pages: List<TransformablePage<T>>,
     placeholdersBefore: Int,
     placeholdersAfter: Int,
-) : NullPaddedList<T> {
+) : PlaceholderPaddedList<T> {
     constructor(
         insertEvent: PageEvent.Insert<T>
     ) : this(
diff --git a/paging/paging-common/src/commonMain/kotlin/androidx/paging/PagingDataEvent.kt b/paging/paging-common/src/commonMain/kotlin/androidx/paging/PagingDataEvent.kt
index 68a0183..1cf0858 100644
--- a/paging/paging-common/src/commonMain/kotlin/androidx/paging/PagingDataEvent.kt
+++ b/paging/paging-common/src/commonMain/kotlin/androidx/paging/PagingDataEvent.kt
@@ -22,7 +22,6 @@
 /**
  * Events captured from a [PagingData] that was submitted to the [PagingDataPresenter]
  */
-@RestrictTo(LIBRARY_GROUP)
 public sealed class PagingDataEvent<T : Any> {
     /**
      * A prepend load event
@@ -108,14 +107,14 @@
     /**
      * A refresh load event
      *
-     * @param [newList] A [NullPaddedList] that contains the metadata of the new list
+     * @param [newList] A [PlaceholderPaddedList] that contains the metadata of the new list
      * that is presented upon this refresh event
-     * @param [previousList] A [NullPaddedList] that contains the metadata of the list
+     * @param [previousList] A [PlaceholderPaddedList] that contains the metadata of the list
      * presented prior to this refresh load event
      */
     public class Refresh<T : Any> @RestrictTo(LIBRARY_GROUP) constructor(
-        val newList: NullPaddedList<T>,
-        val previousList: NullPaddedList<T>,
+        val newList: PlaceholderPaddedList<T>,
+        val previousList: PlaceholderPaddedList<T>,
     ) : PagingDataEvent<T>() {
         override fun equals(other: Any?): Boolean {
             return other is Refresh<*> &&
diff --git a/paging/paging-common/src/commonMain/kotlin/androidx/paging/PagingDataPresenter.kt b/paging/paging-common/src/commonMain/kotlin/androidx/paging/PagingDataPresenter.kt
index 94d8c9d..17fa56a 100644
--- a/paging/paging-common/src/commonMain/kotlin/androidx/paging/PagingDataPresenter.kt
+++ b/paging/paging-common/src/commonMain/kotlin/androidx/paging/PagingDataPresenter.kt
@@ -27,6 +27,7 @@
 import androidx.paging.internal.CopyOnWriteArrayList
 import androidx.paging.internal.appendMediatorStatesIfNotNull
 import kotlin.coroutines.CoroutineContext
+import kotlin.jvm.JvmSuppressWildcards
 import kotlin.jvm.Volatile
 import kotlinx.coroutines.Dispatchers
 import kotlinx.coroutines.channels.BufferOverflow.DROP_OLDEST
@@ -35,10 +36,31 @@
 import kotlinx.coroutines.flow.StateFlow
 import kotlinx.coroutines.flow.asSharedFlow
 import kotlinx.coroutines.withContext
-import kotlinx.coroutines.yield
 
-@RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
-public abstract class PagingDataPresenter<T : Any>(
+/**
+ * The class that connects the UI layer to the underlying Paging operations. Takes input from
+ * UI presenters and outputs Paging events (Loads, LoadStateUpdate) in response.
+ *
+ * Paging front ends that implement this class will be able to access loaded data, LoadStates,
+ * and callbacks from LoadState or Page updates. This class also exposes the
+ * [PagingDataEvent] from a [PagingData] for custom logic on how to present Loads, Drops, and
+ * other Paging events.
+ *
+ * For implementation examples, refer to [AsyncPagingDataDiffer] for RecyclerView,
+ * or [LazyPagingItems] for Compose.
+ *
+ * @param [mainContext] The coroutine context that core Paging operations will run on.
+ * Defaults to [Dispatchers.Main]. Main operations executed within this context include
+ * but are not limited to:
+ * 1. flow collection on a [PagingData] for Loads, LoadStateUpdate etc.
+ * 2. emitting [CombinedLoadStates] to the [loadStateFlow]
+ * 3. invoking LoadState and PageUpdate listeners
+ * 4. invoking [presentPagingDataEvent]
+ *
+ * @param [cachedPagingData] a [PagingData] that will initialize this PagingDataPresenter with
+ * any LoadStates or loaded data contained within it.
+ */
+public abstract class PagingDataPresenter<T : Any> (
     private val mainContext: CoroutineContext = Dispatchers.Main,
     cachedPagingData: PagingData<T>? = null,
 ) {
@@ -73,16 +95,23 @@
     private var lastAccessedIndex: Int = 0
 
     /**
-     * Handler for [PagingDataEvent] emitted by a [PagingData] that was submitted to
-     * this [PagingDataPresenter]
+     * Handler for [PagingDataEvent] emitted by [PagingData].
+     *
+     * When a [PagingData] is submitted to this PagingDataPresenter through [collectFrom],
+     * page loads, drops, or LoadStateUpdates will be emitted to presenters as [PagingDataEvent]
+     * through this method.
+     *
+     * Presenter layers that communicate directly with [PagingDataPresenter] should override
+     * this method to handle the [PagingDataEvent] accordingly. For example by diffing two
+     * [PagingDataEvent.Refresh] lists, or appending the inserted list of data from
+     * [PagingDataEvent.Prepend] or [PagingDataEvent.Append].
+     *
      */
     public abstract suspend fun presentPagingDataEvent(
         event: PagingDataEvent<T>,
-    )
+    ): @JvmSuppressWildcards Unit
 
-    public open fun postEvents(): Boolean = false
-
-    public suspend fun collectFrom(pagingData: PagingData<T>) {
+    public suspend fun collectFrom(pagingData: PagingData<T>): @JvmSuppressWildcards Unit {
         collectFromRunner.runInIsolation {
             uiReceiver = pagingData.uiReceiver
             pagingData.flow.collect { event ->
@@ -133,10 +162,6 @@
                             )
                         }
                         event is Insert -> {
-                            if (postEvents()) {
-                                yield()
-                            }
-
                             // Process APPEND/PREPEND and send to presenter
                             presentPagingDataEvent(pageStore.processEvent(event))
 
@@ -189,10 +214,6 @@
                             }
                         }
                         event is Drop -> {
-                            if (postEvents()) {
-                                yield()
-                            }
-
                             // Process DROP and send to presenter
                             presentPagingDataEvent(pageStore.processEvent(event))
 
@@ -207,11 +228,12 @@
                             // infinite loops when maxSize is insufficiently large.
                             lastAccessedIndexUnfulfilled = false
                         }
-                        event is PageEvent.LoadStateUpdate ->
+                        event is PageEvent.LoadStateUpdate -> {
                             combinedLoadStatesCollection.set(
                                 sourceLoadStates = event.source,
                                 remoteLoadStates = event.mediator,
                             )
+                        }
                     }
                     // Notify page updates after pageStore processes them.
                     //
@@ -395,7 +417,7 @@
      *
      * @sample androidx.paging.samples.addLoadStateListenerSample
      */
-    public fun addLoadStateListener(listener: (CombinedLoadStates) -> Unit) {
+    public fun addLoadStateListener(listener: (@JvmSuppressWildcards CombinedLoadStates) -> Unit) {
         combinedLoadStatesCollection.addListener(listener)
     }
 
@@ -405,7 +427,9 @@
      * @param listener Previously registered listener.
      * @see addLoadStateListener
      */
-    public fun removeLoadStateListener(listener: (CombinedLoadStates) -> Unit) {
+    public fun removeLoadStateListener(
+        listener: (@JvmSuppressWildcards CombinedLoadStates) -> Unit
+    ) {
         combinedLoadStatesCollection.removeListener(listener)
     }
 
@@ -430,7 +454,7 @@
             placeholdersAfter = placeholdersAfter,
         )
         // must capture previousList states here before we update pageStore
-        val previousList = pageStore as NullPaddedList<T>
+        val previousList = pageStore as PlaceholderPaddedList<T>
 
         // update the store here before event is sent to ensure that snapshot() returned in
         // UI update callbacks (onChanged, onInsert etc) reflects the new list
@@ -440,7 +464,7 @@
         // send event to UI
         presentPagingDataEvent(
             PagingDataEvent.Refresh(
-                newList = newPageStore as NullPaddedList<T>,
+                newList = newPageStore as PlaceholderPaddedList<T>,
                 previousList = previousList,
             )
         )
diff --git a/paging/paging-common/src/commonMain/kotlin/androidx/paging/NullPaddedList.kt b/paging/paging-common/src/commonMain/kotlin/androidx/paging/PlaceholderPaddedList.kt
similarity index 68%
rename from paging/paging-common/src/commonMain/kotlin/androidx/paging/NullPaddedList.kt
rename to paging/paging-common/src/commonMain/kotlin/androidx/paging/PlaceholderPaddedList.kt
index f434749..f9dc9ec 100644
--- a/paging/paging-common/src/commonMain/kotlin/androidx/paging/NullPaddedList.kt
+++ b/paging/paging-common/src/commonMain/kotlin/androidx/paging/PlaceholderPaddedList.kt
@@ -16,15 +16,15 @@
 
 package androidx.paging
 
-import androidx.annotation.RestrictTo
-
 /**
- * Interface to partially-loaded, paged data (generally an immutable snapshot).
+ * Interface to paged list that could contain placeholders.
  *
- * Used for diffing in paging-runtime.
+ * Contains a paged list's snapshot state. For example, in the context of
+ * [PagingDataEvent.Refresh] exposed by [PagingDataPresenter], each [PlaceholderPaddedList]
+ * represents a generation of paged data whereby a new generation is distinguished with
+ * a refresh load.
  */
-@RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
-public interface NullPaddedList<T> {
+public interface PlaceholderPaddedList<T> {
     public val placeholdersBefore: Int
     public val placeholdersAfter: Int
     public val size: Int
diff --git a/paging/paging-common/src/commonTest/kotlin/androidx/paging/PagingDataPresenterTest.kt b/paging/paging-common/src/commonTest/kotlin/androidx/paging/PagingDataPresenterTest.kt
index fdb2710..96166ac 100644
--- a/paging/paging-common/src/commonTest/kotlin/androidx/paging/PagingDataPresenterTest.kt
+++ b/paging/paging-common/src/commonTest/kotlin/androidx/paging/PagingDataPresenterTest.kt
@@ -1444,12 +1444,12 @@
                 ),
                 placeholdersBefore = 0,
                 placeholdersAfter = 0,
-            ) as NullPaddedList<Int>
+            ) as PlaceholderPaddedList<Int>
 
             assertThat(presenter.snapshot()).containsExactlyElementsIn(50 until 59)
             assertThat(presenter.newEvents()).containsExactly(
                 PagingDataEvent.Refresh(
-                    previousList = PageStore.initial<Int>(null) as NullPaddedList<Int>,
+                    previousList = PageStore.initial<Int>(null) as PlaceholderPaddedList<Int>,
                     newList = event
                 )
             )
@@ -1475,7 +1475,7 @@
                         ),
                         placeholdersBefore = 0,
                         placeholdersAfter = 0,
-                    ) as NullPaddedList<Int>
+                    ) as PlaceholderPaddedList<Int>
                 )
             )
         }
diff --git a/paging/paging-runtime/build.gradle b/paging/paging-runtime/build.gradle
index 3f072d0..d6d8fa0 100644
--- a/paging/paging-runtime/build.gradle
+++ b/paging/paging-runtime/build.gradle
@@ -53,6 +53,7 @@
     androidTestImplementation(project(":internal-testutils-common"))
     androidTestImplementation(project(":internal-testutils-ktx"))
     androidTestImplementation(project(":internal-testutils-paging"))
+    androidTestImplementation(project(":paging:paging-testing"))
     androidTestImplementation(projectOrArtifact(":lifecycle:lifecycle-runtime-testing"))
     androidTestImplementation(libs.testCore)
     androidTestImplementation(libs.testExtJunit)
diff --git a/paging/paging-runtime/src/androidTest/java/androidx/paging/AsyncPagingDataDifferTest.kt b/paging/paging-runtime/src/androidTest/java/androidx/paging/AsyncPagingDataDifferTest.kt
index d95155c..98b8b06 100644
--- a/paging/paging-runtime/src/androidTest/java/androidx/paging/AsyncPagingDataDifferTest.kt
+++ b/paging/paging-runtime/src/androidTest/java/androidx/paging/AsyncPagingDataDifferTest.kt
@@ -40,12 +40,15 @@
 import kotlinx.coroutines.ExperimentalCoroutinesApi
 import kotlinx.coroutines.Runnable
 import kotlinx.coroutines.cancelAndJoin
+import kotlinx.coroutines.flow.collect
 import kotlinx.coroutines.flow.collectLatest
+import kotlinx.coroutines.flow.filterNotNull
 import kotlinx.coroutines.flow.first
 import kotlinx.coroutines.launch
 import kotlinx.coroutines.test.StandardTestDispatcher
 import kotlinx.coroutines.test.TestScope
 import kotlinx.coroutines.test.advanceUntilIdle
+import kotlinx.coroutines.test.resetMain
 import kotlinx.coroutines.test.runTest
 import kotlinx.coroutines.withContext
 import org.junit.Rule
@@ -716,6 +719,400 @@
     }
 
     @Test
+    fun loadStateListenerYieldsToRecyclerView() {
+        Dispatchers.resetMain() // reset MainDispatcherRule
+        // collection on immediate dispatcher to simulate real lifecycle dispatcher
+        val mainDispatcher = Dispatchers.Main.immediate
+        runTest {
+            val events = mutableListOf<String>()
+            val asyncDiffer = AsyncPagingDataDiffer(
+                diffCallback = object : DiffUtil.ItemCallback<Int>() {
+                    override fun areContentsTheSame(oldItem: Int, newItem: Int): Boolean {
+                        return oldItem == newItem
+                    }
+
+                    override fun areItemsTheSame(oldItem: Int, newItem: Int): Boolean {
+                        return oldItem == newItem
+                    }
+                },
+                // override default Dispatcher.Main with Dispatchers.main.immediate so that
+                // main tasks run without queueing, we need this to simulate real life order of
+                // events
+                mainDispatcher = mainDispatcher,
+                updateCallback = listUpdateCapture,
+                workerDispatcher = backgroundScope.coroutineContext
+            )
+
+            val pager = Pager(
+                config = PagingConfig(
+                    pageSize = 10,
+                    enablePlaceholders = false,
+                    prefetchDistance = 3,
+                    initialLoadSize = 10,
+                )
+            ) { TestPagingSource() }
+
+            asyncDiffer.addLoadStateListener {
+                events.add(it.toString())
+            }
+
+            val collectPager = launch(mainDispatcher) {
+                pager.flow.collectLatest { asyncDiffer.submitData(it) }
+            }
+
+            // wait till we get all expected events
+            asyncDiffer.loadStateFlow.awaitNotLoading()
+
+            assertThat(events).containsExactly(
+                localLoadStatesOf(refreshLocal = Loading).toString(),
+                localLoadStatesOf(prependLocal = NotLoading(true)).toString()
+            ).inOrder()
+            events.clear()
+
+            // Simulate RV dispatch layout which calls multi onBind --> getItem. LoadStateUpdates
+            // from upstream should yield until dispatch layout completes or else
+            // LoadState-based RV updates will crash. See original bug b/150162465.
+            withContext(mainDispatcher) {
+                events.add("start dispatchLayout")
+                asyncDiffer.getItem(6)
+                asyncDiffer.getItem(7) // this triggers load
+                asyncDiffer.getItem(8)
+                events.add("end dispatchLayout")
+            }
+
+            // wait till we get all expected events
+            asyncDiffer.loadStateFlow.awaitNotLoading()
+
+            // make sure we received the LoadStateUpdate only after dispatchLayout ended
+            assertThat(events).containsExactly(
+                "start dispatchLayout",
+                "end dispatchLayout",
+                localLoadStatesOf(
+                    appendLocal = Loading,
+                    prependLocal = NotLoading(true)
+                ).toString(),
+                localLoadStatesOf(prependLocal = NotLoading(true)).toString()
+            ).inOrder()
+
+            collectPager.cancel()
+        }
+    }
+
+    @Test
+    fun loadStateFlowYieldsToRecyclerView() {
+        Dispatchers.resetMain() // reset MainDispatcherRule
+        // collection on immediate dispatcher to simulate real lifecycle dispatcher
+        val mainDispatcher = Dispatchers.Main.immediate
+        runTest {
+            val events = mutableListOf<String>()
+            val asyncDiffer = AsyncPagingDataDiffer(
+                diffCallback = object : DiffUtil.ItemCallback<Int>() {
+                    override fun areContentsTheSame(oldItem: Int, newItem: Int): Boolean {
+                        return oldItem == newItem
+                    }
+
+                    override fun areItemsTheSame(oldItem: Int, newItem: Int): Boolean {
+                        return oldItem == newItem
+                    }
+                },
+                // override default Dispatcher.Main with Dispatchers.main.immediate so that
+                // main tasks run without queueing, we need this to simulate real life order of
+                // events
+                mainDispatcher = mainDispatcher,
+                updateCallback = listUpdateCapture,
+                workerDispatcher = backgroundScope.coroutineContext
+            )
+
+            val pager = Pager(
+                config = PagingConfig(
+                    pageSize = 10,
+                    enablePlaceholders = false,
+                    prefetchDistance = 3,
+                    initialLoadSize = 10,
+                )
+            ) { TestPagingSource() }
+
+            val collectLoadState = launch(mainDispatcher) {
+                asyncDiffer.loadStateFlow.collect {
+                    events.add(it.toString())
+                }
+            }
+
+            val collectPager = launch(mainDispatcher) {
+                pager.flow.collectLatest { asyncDiffer.submitData(it) }
+            }
+
+            // wait till we get all expected events
+            asyncDiffer.loadStateFlow.awaitNotLoading()
+
+            // withContext prevents flake in API 28 where sometimes we start asserting before
+            // the NotLoading state is added to events list
+            assertThat(events).containsExactly(
+                localLoadStatesOf(refreshLocal = Loading).toString(),
+                localLoadStatesOf(prependLocal = NotLoading(true)).toString()
+            ).inOrder()
+
+            events.clear()
+
+            // Simulate RV dispatching layout which calls multi onBind --> getItem. LoadStateUpdates
+            // from upstream should yield until dispatch layout completes or else
+            // LoadState-based RV updates will crash. See original bug b/150162465.
+            withContext(mainDispatcher) {
+                events.add("start dispatchLayout")
+                asyncDiffer.getItem(6)
+                asyncDiffer.getItem(7) // this triggers load
+                asyncDiffer.getItem(8)
+                events.add("end dispatchLayout")
+            }
+
+            // wait till we get all expected events
+            asyncDiffer.loadStateFlow.awaitNotLoading()
+
+            // make sure we received the LoadStateUpdate only after dispatchLayout ended
+            assertThat(events).containsExactly(
+                "start dispatchLayout",
+                "end dispatchLayout",
+                localLoadStatesOf(
+                    appendLocal = Loading,
+                    prependLocal = NotLoading(true)
+                ).toString(),
+                localLoadStatesOf(prependLocal = NotLoading(true)).toString()
+            ).inOrder()
+
+            collectLoadState.cancel()
+            collectPager.cancel()
+        }
+    }
+
+    @Test
+    fun loadStateFlowYieldsToGetItem() {
+        Dispatchers.resetMain() // reset MainDispatcherRule
+        // collection on immediate dispatcher to simulate real lifecycle dispatcher
+        val mainDispatcher = Dispatchers.Main.immediate
+        runTest {
+            val events = mutableListOf<String>()
+            val asyncDiffer = AsyncPagingDataDiffer(
+                diffCallback = object : DiffUtil.ItemCallback<Int>() {
+                    override fun areContentsTheSame(oldItem: Int, newItem: Int): Boolean {
+                        return oldItem == newItem
+                    }
+
+                    override fun areItemsTheSame(oldItem: Int, newItem: Int): Boolean {
+                        return oldItem == newItem
+                    }
+                },
+                // override default Dispatcher.Main with Dispatchers.main.immediate so that
+                // main tasks run without queueing, we need this to simulate real life order of
+                // events
+                mainDispatcher = mainDispatcher,
+                updateCallback = listUpdateCapture,
+                workerDispatcher = backgroundScope.coroutineContext
+            )
+
+            val pager = Pager(
+                config = PagingConfig(
+                    pageSize = 10,
+                    enablePlaceholders = false,
+                    prefetchDistance = 3,
+                    initialLoadSize = 10,
+                )
+            ) { TestPagingSource() }
+
+            val collectInGetItem = launch(mainDispatcher) {
+                asyncDiffer.inGetItem.collect {
+                    events.add("inGetItem $it")
+                }
+            }
+
+            val collectLoadState = launch(mainDispatcher) {
+                asyncDiffer.loadStateFlow.collect {
+                    events.add(it.toString())
+                }
+            }
+
+            // since we cannot intercept the internal loadStateFlow, we collect from its source
+            // flow to see when the internal flow first collected the LoadState before
+            // waiting for getItem
+            val collectParallelLoadState = launch(mainDispatcher) {
+                asyncDiffer.presenter.loadStateFlow.filterNotNull().collect {
+                    events.add("internal flow collected")
+                }
+            }
+
+            val collectPager = launch(mainDispatcher) {
+                pager.flow.collectLatest { asyncDiffer.submitData(it) }
+            }
+
+            // wait till we get all expected events
+            asyncDiffer.loadStateFlow.awaitNotLoading()
+
+            assertThat(events).containsExactly(
+                "inGetItem false",
+                "internal flow collected",
+                localLoadStatesOf(refreshLocal = Loading).toString(),
+                "internal flow collected",
+                localLoadStatesOf(prependLocal = NotLoading(true)).toString()
+            ).inOrder()
+
+            // reset events count
+            events.clear()
+
+            // Simulate RV dispatching layout which calls multi onBind --> getItem. LoadStateUpdates
+            // from upstream should yield until dispatch layout completes or else
+            // LoadState-based RV updates will crash. See original bug b/150162465.
+            withContext(mainDispatcher) {
+                events.add("start dispatchLayout")
+                asyncDiffer.getItem(6)
+                asyncDiffer.getItem(7) // this triggers load
+                asyncDiffer.getItem(8)
+                events.add("end dispatchLayout")
+            }
+
+            // wait till we get all expected events
+            asyncDiffer.loadStateFlow.awaitNotLoading()
+
+            // assert two things: loadStates were received after dispatchLayout and that
+            // the internal flow did collect a LoadState while inGetItem is true but had waited
+            assertThat(events).containsExactly(
+                "start dispatchLayout",
+                // getItem(6)
+                "inGetItem true",
+                "inGetItem false",
+                // getItem(7) triggers append
+                "inGetItem true",
+                "internal flow collected",
+                "inGetItem false",
+                // getItem(8)
+                "inGetItem true",
+                "inGetItem false",
+                "end dispatchLayout",
+                localLoadStatesOf(
+                    appendLocal = Loading,
+                    prependLocal = NotLoading(true)
+                ).toString(),
+                "internal flow collected",
+                localLoadStatesOf(prependLocal = NotLoading(true)).toString()
+            ).inOrder()
+
+            collectInGetItem.cancel()
+            collectLoadState.cancel()
+            collectParallelLoadState.cancel()
+            collectPager.cancel()
+        }
+    }
+
+    @Test
+    fun loadStateListenerYieldsToGetItem() {
+        Dispatchers.resetMain() // reset MainDispatcherRule
+        // collection on immediate dispatcher to simulate real lifecycle dispatcher
+        val mainDispatcher = Dispatchers.Main.immediate
+        runTest {
+            val events = mutableListOf<String>()
+            val asyncDiffer = AsyncPagingDataDiffer(
+                diffCallback = object : DiffUtil.ItemCallback<Int>() {
+                    override fun areContentsTheSame(oldItem: Int, newItem: Int): Boolean {
+                        return oldItem == newItem
+                    }
+
+                    override fun areItemsTheSame(oldItem: Int, newItem: Int): Boolean {
+                        return oldItem == newItem
+                    }
+                },
+                // override default Dispatcher.Main with Dispatchers.main.immediate so that
+                // main tasks run without queueing, we need this to simulate real life order of
+                // events
+                mainDispatcher = mainDispatcher,
+                updateCallback = listUpdateCapture,
+                workerDispatcher = backgroundScope.coroutineContext
+            )
+
+            val pager = Pager(
+                config = PagingConfig(
+                    pageSize = 10,
+                    enablePlaceholders = false,
+                    prefetchDistance = 3,
+                    initialLoadSize = 10,
+                )
+            ) { TestPagingSource() }
+
+            val collectInGetItem = launch(mainDispatcher) {
+                asyncDiffer.inGetItem.collect {
+                    events.add("inGetItem $it")
+                }
+            }
+
+            // override internal loadStateListener that is registered with PagingDataPresenter
+            asyncDiffer.addLoadStateListenerInternal {
+                events.add("internal listener invoked")
+                asyncDiffer.internalLoadStateListener.invoke(it)
+            }
+
+            // add actual UI listener
+            asyncDiffer.addLoadStateListener {
+                events.add(it.toString())
+            }
+
+            val collectPager = launch(mainDispatcher) {
+                pager.flow.collectLatest { asyncDiffer.submitData(it) }
+            }
+
+            // wait till we get all expected events
+            asyncDiffer.loadStateFlow.awaitNotLoading()
+
+            assertThat(events).containsExactly(
+                "inGetItem false",
+                "internal listener invoked",
+                localLoadStatesOf(refreshLocal = Loading).toString(),
+                "internal listener invoked",
+                localLoadStatesOf(prependLocal = NotLoading(true)).toString()
+            ).inOrder()
+
+            // reset events count
+            events.clear()
+
+            // Simulate RV dispatching layout which calls multi onBind --> getItem. LoadStateUpdates
+            // from upstream should yield until dispatch layout completes or else
+            // LoadState-based RV updates will crash. See original bug b/150162465.
+            withContext(mainDispatcher) {
+                events.add("start dispatchLayout")
+                asyncDiffer.getItem(6)
+                asyncDiffer.getItem(7) // this triggers load
+                asyncDiffer.getItem(8)
+                events.add("end dispatchLayout")
+            }
+
+            // wait till we get all expected events
+            asyncDiffer.loadStateFlow.awaitNotLoading()
+
+            // assert two things: loadStates were received after dispatchLayout and that
+            // the internal listener was invoked while inGetItem is true but had waited
+            assertThat(events).containsExactly(
+                "start dispatchLayout",
+                // getItem(6)
+                "inGetItem true",
+                "inGetItem false",
+                // getItem(7) triggers append
+                "inGetItem true",
+                "internal listener invoked",
+                "inGetItem false",
+                // getItem(8)
+                "inGetItem true",
+                "inGetItem false",
+                "end dispatchLayout",
+                localLoadStatesOf(
+                    appendLocal = Loading,
+                    prependLocal = NotLoading(true)
+                ).toString(),
+                "internal listener invoked",
+                localLoadStatesOf(prependLocal = NotLoading(true)).toString()
+            ).inOrder()
+
+            collectInGetItem.cancel()
+            collectPager.cancel()
+        }
+    }
+
+    @Test
     fun insertPageEmpty() = verifyPrependAppendCallback(
         initialItems = 2,
         initialNulls = 0,
diff --git a/paging/paging-runtime/src/androidTest/java/androidx/paging/NullPaddedListDiffHelperTest.kt b/paging/paging-runtime/src/androidTest/java/androidx/paging/PlaceholderPaddedListDiffHelperTest.kt
similarity index 99%
rename from paging/paging-runtime/src/androidTest/java/androidx/paging/NullPaddedListDiffHelperTest.kt
rename to paging/paging-runtime/src/androidTest/java/androidx/paging/PlaceholderPaddedListDiffHelperTest.kt
index 6b27bef..9a2eb07 100644
--- a/paging/paging-runtime/src/androidTest/java/androidx/paging/NullPaddedListDiffHelperTest.kt
+++ b/paging/paging-runtime/src/androidTest/java/androidx/paging/PlaceholderPaddedListDiffHelperTest.kt
@@ -33,12 +33,12 @@
 
 @SmallTest
 @RunWith(AndroidJUnit4::class)
-class NullPaddedListDiffHelperTest {
+class PlaceholderPaddedListDiffHelperTest {
     class Storage(
         override val placeholdersBefore: Int,
         private val data: List<String>,
         override val placeholdersAfter: Int
-    ) : NullPaddedList<String> {
+    ) : PlaceholderPaddedList<String> {
         override fun getItem(index: Int): String = data[index]
         override val size: Int
             get() = placeholdersBefore + data.size + placeholdersAfter
diff --git a/paging/paging-runtime/src/androidTest/java/androidx/paging/NullPaddedListDiffWithRecyclerViewTest.kt b/paging/paging-runtime/src/androidTest/java/androidx/paging/PlaceholderPaddedListDiffWithRecyclerViewTest.kt
similarity index 86%
rename from paging/paging-runtime/src/androidTest/java/androidx/paging/NullPaddedListDiffWithRecyclerViewTest.kt
rename to paging/paging-runtime/src/androidTest/java/androidx/paging/PlaceholderPaddedListDiffWithRecyclerViewTest.kt
index bc35df8..812da03 100644
--- a/paging/paging-runtime/src/androidTest/java/androidx/paging/NullPaddedListDiffWithRecyclerViewTest.kt
+++ b/paging/paging-runtime/src/androidTest/java/androidx/paging/PlaceholderPaddedListDiffWithRecyclerViewTest.kt
@@ -41,10 +41,10 @@
  */
 @RunWith(AndroidJUnit4::class)
 @LargeTest
-class NullPaddedListDiffWithRecyclerViewTest {
+class PlaceholderPaddedListDiffWithRecyclerViewTest {
     private lateinit var context: Context
     private lateinit var recyclerView: RecyclerView
-    private lateinit var adapter: NullPaddedListAdapter
+    private lateinit var adapter: PlaceholderPaddedListAdapter
 
     @Before
     fun init() {
@@ -55,7 +55,7 @@
             it.layoutManager = LinearLayoutManager(context)
             it.itemAnimator = null
         }
-        adapter = NullPaddedListAdapter()
+        adapter = PlaceholderPaddedListAdapter()
         recyclerView.adapter = adapter
     }
 
@@ -71,7 +71,7 @@
 
     @Test
     fun basic() {
-        val storage = NullPaddedStorage(
+        val storage = PlaceholderPaddedStorage(
             placeholdersBefore = 0,
             data = createItems(0, 10),
             placeholdersAfter = 0
@@ -90,12 +90,12 @@
 
     @Test
     fun distinctLists_fullyOverlappingRange() {
-        val pre = NullPaddedStorage(
+        val pre = PlaceholderPaddedStorage(
             placeholdersBefore = 10,
             data = createItems(startId = 10, count = 8),
             placeholdersAfter = 30
         )
-        val post = NullPaddedStorage(
+        val post = PlaceholderPaddedStorage(
             placeholdersBefore = 10,
             data = createItems(startId = 100, count = 8),
             placeholdersAfter = 30
@@ -108,12 +108,12 @@
 
     @Test
     fun distinctLists_loadedBefore_or_After() {
-        val pre = NullPaddedStorage(
+        val pre = PlaceholderPaddedStorage(
             placeholdersBefore = 10,
             data = createItems(startId = 10, count = 10),
             placeholdersAfter = 10
         )
-        val post = NullPaddedStorage(
+        val post = PlaceholderPaddedStorage(
             placeholdersBefore = 5,
             data = createItems(startId = 5, count = 5),
             placeholdersAfter = 20
@@ -126,12 +126,12 @@
 
     @Test
     fun distinctLists_partiallyOverlapping() {
-        val pre = NullPaddedStorage(
+        val pre = PlaceholderPaddedStorage(
             placeholdersBefore = 10,
             data = createItems(startId = 0, count = 8),
             placeholdersAfter = 30
         )
-        val post = NullPaddedStorage(
+        val post = PlaceholderPaddedStorage(
             placeholdersBefore = 15,
             data = createItems(startId = 100, count = 8),
             placeholdersAfter = 30
@@ -144,12 +144,12 @@
 
     @Test
     fun distinctLists_fewerItemsLoaded_withMorePlaceholdersBefore() {
-        val pre = NullPaddedStorage(
+        val pre = PlaceholderPaddedStorage(
             placeholdersBefore = 10,
             data = createItems(startId = 10, count = 8),
             placeholdersAfter = 30
         )
-        val post = NullPaddedStorage(
+        val post = PlaceholderPaddedStorage(
             placeholdersBefore = 15,
             data = createItems(startId = 100, count = 3),
             placeholdersAfter = 30
@@ -162,12 +162,12 @@
 
     @Test
     fun distinctLists_noPlaceholdersLeft() {
-        val pre = NullPaddedStorage(
+        val pre = PlaceholderPaddedStorage(
             placeholdersBefore = 10,
             data = createItems(startId = 10, count = 8),
             placeholdersAfter = 30
         )
-        val post = NullPaddedStorage(
+        val post = PlaceholderPaddedStorage(
             placeholdersBefore = 0,
             data = createItems(startId = 100, count = 3),
             placeholdersAfter = 0
@@ -180,12 +180,12 @@
 
     @Test
     fun distinctLists_moreItemsLoaded() {
-        val pre = NullPaddedStorage(
+        val pre = PlaceholderPaddedStorage(
             placeholdersBefore = 10,
             data = createItems(startId = 10, count = 3),
             placeholdersAfter = 30
         )
-        val post = NullPaddedStorage(
+        val post = PlaceholderPaddedStorage(
             placeholdersBefore = 10,
             data = createItems(startId = 100, count = 8),
             placeholdersAfter = 30
@@ -198,12 +198,12 @@
 
     @Test
     fun distinctLists_moreItemsLoaded_andAlsoMoreOffset() {
-        val pre = NullPaddedStorage(
+        val pre = PlaceholderPaddedStorage(
             placeholdersBefore = 10,
             data = createItems(startId = 10, count = 3),
             placeholdersAfter = 30
         )
-        val post = NullPaddedStorage(
+        val post = PlaceholderPaddedStorage(
             placeholdersBefore = 15,
             data = createItems(startId = 100, count = 8),
             placeholdersAfter = 30
@@ -216,12 +216,12 @@
 
     @Test
     fun distinctLists_expandShrink() {
-        val pre = NullPaddedStorage(
+        val pre = PlaceholderPaddedStorage(
             placeholdersBefore = 10,
             data = createItems(10, 10),
             placeholdersAfter = 20
         )
-        val post = NullPaddedStorage(
+        val post = PlaceholderPaddedStorage(
             placeholdersBefore = 0,
             data = createItems(100, 1),
             placeholdersAfter = 0
@@ -236,8 +236,8 @@
      * Runs a state restoration test with various "current scroll positions".
      */
     private fun distinctListTest_withVariousInitialPositions(
-        pre: NullPaddedStorage,
-        post: NullPaddedStorage
+        pre: PlaceholderPaddedStorage,
+        post: PlaceholderPaddedStorage
     ) {
         // try restoring positions in different list states
         val minSize = minOf(pre.size, post.size)
@@ -260,12 +260,12 @@
 
     @Test
     fun distinctLists_visibleRangeRemoved() {
-        val pre = NullPaddedStorage(
+        val pre = PlaceholderPaddedStorage(
             placeholdersBefore = 10,
             data = createItems(10, 10),
             placeholdersAfter = 30
         )
-        val post = NullPaddedStorage(
+        val post = PlaceholderPaddedStorage(
             placeholdersBefore = 0,
             data = createItems(100, 4),
             placeholdersAfter = 20
@@ -289,12 +289,12 @@
 
     @Test
     fun distinctLists_validateDiff() {
-        val pre = NullPaddedStorage(
+        val pre = PlaceholderPaddedStorage(
             placeholdersBefore = 10,
             data = createItems(10, 10), // their positions won't be in the new list
             placeholdersAfter = 20
         )
-        val post = NullPaddedStorage(
+        val post = PlaceholderPaddedStorage(
             placeholdersBefore = 0,
             data = createItems(100, 1),
             placeholdersAfter = 0
@@ -308,7 +308,7 @@
         // this is a random test but if it fails, the exception will have enough information to
         // create an isolated test
         val rand = Random(System.nanoTime())
-        fun randomNullPaddedStorage(startId: Int) = NullPaddedStorage(
+        fun randomPlaceholderPaddedStorage(startId: Int) = PlaceholderPaddedStorage(
             placeholdersBefore = rand.nextInt(0, 20),
             data = createItems(
                 startId = startId,
@@ -318,20 +318,20 @@
         )
         repeat(RANDOM_TEST_REPEAT_SIZE) {
             updateDiffTest(
-                pre = randomNullPaddedStorage(0),
-                post = randomNullPaddedStorage(1_000)
+                pre = randomPlaceholderPaddedStorage(0),
+                post = randomPlaceholderPaddedStorage(1_000)
             )
         }
     }
 
     @Test
     fun continuousMatch_1() {
-        val pre = NullPaddedStorage(
+        val pre = PlaceholderPaddedStorage(
             placeholdersBefore = 4,
             data = createItems(startId = 0, count = 16),
             placeholdersAfter = 1
         )
-        val post = NullPaddedStorage(
+        val post = PlaceholderPaddedStorage(
             placeholdersBefore = 1,
             data = createItems(startId = 13, count = 4),
             placeholdersAfter = 19
@@ -341,12 +341,12 @@
 
     @Test
     fun continuousMatch_2() {
-        val pre = NullPaddedStorage(
+        val pre = PlaceholderPaddedStorage(
             placeholdersBefore = 6,
             data = createItems(startId = 0, count = 9),
             placeholdersAfter = 19
         )
-        val post = NullPaddedStorage(
+        val post = PlaceholderPaddedStorage(
             placeholdersBefore = 14,
             data = createItems(startId = 4, count = 3),
             placeholdersAfter = 11
@@ -356,12 +356,12 @@
 
     @Test
     fun continuousMatch_3() {
-        val pre = NullPaddedStorage(
+        val pre = PlaceholderPaddedStorage(
             placeholdersBefore = 11,
             data = createItems(startId = 0, count = 4),
             placeholdersAfter = 6
         )
-        val post = NullPaddedStorage(
+        val post = PlaceholderPaddedStorage(
             placeholdersBefore = 7,
             data = createItems(startId = 0, count = 1),
             placeholdersAfter = 11
@@ -371,12 +371,12 @@
 
     @Test
     fun continuousMatch_4() {
-        val pre = NullPaddedStorage(
+        val pre = PlaceholderPaddedStorage(
             placeholdersBefore = 4,
             data = createItems(startId = 0, count = 15),
             placeholdersAfter = 18
         )
-        val post = NullPaddedStorage(
+        val post = PlaceholderPaddedStorage(
             placeholdersBefore = 11,
             data = createItems(startId = 5, count = 17),
             placeholdersAfter = 9
@@ -404,7 +404,7 @@
         // this is a random test but if it fails, the exception will have enough information to
         // create an isolated test
         val rand = Random(System.nanoTime())
-        fun randomNullPaddedStorage(startId: Int) = NullPaddedStorage(
+        fun randomPlaceholderPaddedStorage(startId: Int) = PlaceholderPaddedStorage(
             placeholdersBefore = rand.nextInt(0, 20),
             data = createItems(
                 startId = startId,
@@ -416,8 +416,8 @@
             placeholdersAfter = rand.nextInt(0, 20)
         )
         repeat(RANDOM_TEST_REPEAT_SIZE) {
-            val pre = randomNullPaddedStorage(0)
-            val post = randomNullPaddedStorage(
+            val pre = randomPlaceholderPaddedStorage(0)
+            val post = randomPlaceholderPaddedStorage(
                 startId = if (pre.dataCount > 0) {
                     pre.getItem(rand.nextInt(pre.dataCount)).id
                 } else {
@@ -435,18 +435,18 @@
      * Validates that the update events between [pre] and [post] are correct.
      */
     private fun updateDiffTest(
-        pre: NullPaddedStorage,
-        post: NullPaddedStorage
+        pre: PlaceholderPaddedStorage,
+        post: PlaceholderPaddedStorage
     ) {
         val callback = ValidatingListUpdateCallback(pre, post)
-        val diffResult = pre.computeDiff(post, NullPaddedListItem.CALLBACK)
+        val diffResult = pre.computeDiff(post, PlaceholderPaddedListItem.CALLBACK)
         pre.dispatchDiff(callback, post, diffResult)
         callback.validateRunningListAgainst()
     }
 
     private fun distinctListTest(
-        pre: NullPaddedStorage,
-        post: NullPaddedStorage,
+        pre: PlaceholderPaddedStorage,
+        post: PlaceholderPaddedStorage,
         initialListPos: Int,
         finalListPos: Int = initialListPos
     ) {
@@ -485,8 +485,8 @@
      * with UI snapshots.
      */
     private fun swapListTest(
-        pre: NullPaddedStorage,
-        post: NullPaddedStorage,
+        pre: PlaceholderPaddedStorage,
+        post: PlaceholderPaddedStorage,
         preSwapAction: () -> Unit = {},
         postSwapAction: () -> Unit = {},
         validate: (preCapture: List<UIItemSnapshot>, postCapture: List<UIItemSnapshot>) -> Unit
@@ -509,7 +509,8 @@
         return (0 until recyclerView.childCount).mapNotNull { childPos ->
             val view = recyclerView.getChildAt(childPos)!!
             if (view.top < RV_HEIGHT && view.bottom > 0) {
-                val viewHolder = recyclerView.getChildViewHolder(view) as NullPaddedListViewHolder
+                val viewHolder =
+                    recyclerView.getChildViewHolder(view) as PlaceholderPaddedListViewHolder
                 UIItemSnapshot(
                     top = view.top,
                     boundItem = viewHolder.boundItem,
@@ -524,16 +525,17 @@
     /**
      * Custom adapter class that also validates its update events to ensure they are correct.
      */
-    private class NullPaddedListAdapter : RecyclerView.Adapter<NullPaddedListViewHolder>() {
-        private var items: NullPaddedList<NullPaddedListItem>? = null
+    private class PlaceholderPaddedListAdapter :
+        RecyclerView.Adapter<PlaceholderPaddedListViewHolder>() {
+        private var items: PlaceholderPaddedList<PlaceholderPaddedListItem>? = null
 
-        fun setItems(items: NullPaddedList<NullPaddedListItem>) {
+        fun setItems(items: PlaceholderPaddedList<PlaceholderPaddedListItem>) {
             val previousItems = this.items
             val myItems = this.items
             if (myItems == null) {
                 notifyItemRangeInserted(0, items.size)
             } else {
-                val diff = myItems.computeDiff(items, NullPaddedListItem.CALLBACK)
+                val diff = myItems.computeDiff(items, PlaceholderPaddedListItem.CALLBACK)
                 val diffObserver = TrackingAdapterObserver(previousItems, items)
                 registerAdapterDataObserver(diffObserver)
                 val callback = AdapterListUpdateCallback(this)
@@ -547,8 +549,8 @@
         override fun onCreateViewHolder(
             parent: ViewGroup,
             viewType: Int
-        ): NullPaddedListViewHolder {
-            return NullPaddedListViewHolder(parent.context).also {
+        ): PlaceholderPaddedListViewHolder {
+            return PlaceholderPaddedListViewHolder(parent.context).also {
                 it.itemView.layoutParams = RecyclerView.LayoutParams(
                     RecyclerView.LayoutParams.MATCH_PARENT,
                     ITEM_HEIGHT
@@ -556,7 +558,7 @@
             }
         }
 
-        override fun onBindViewHolder(holder: NullPaddedListViewHolder, position: Int) {
+        override fun onBindViewHolder(holder: PlaceholderPaddedListViewHolder, position: Int) {
             val item = items?.get(position)
             holder.boundItem = item
             holder.boundPos = position
@@ -567,22 +569,22 @@
         }
     }
 
-    private data class NullPaddedListItem(
+    private data class PlaceholderPaddedListItem(
         val id: Int,
         val value: String
     ) {
         companion object {
-            val CALLBACK = object : DiffUtil.ItemCallback<NullPaddedListItem>() {
+            val CALLBACK = object : DiffUtil.ItemCallback<PlaceholderPaddedListItem>() {
                 override fun areItemsTheSame(
-                    oldItem: NullPaddedListItem,
-                    newItem: NullPaddedListItem
+                    oldItem: PlaceholderPaddedListItem,
+                    newItem: PlaceholderPaddedListItem
                 ): Boolean {
                     return oldItem.id == newItem.id
                 }
 
                 override fun areContentsTheSame(
-                    oldItem: NullPaddedListItem,
-                    newItem: NullPaddedListItem
+                    oldItem: PlaceholderPaddedListItem,
+                    newItem: PlaceholderPaddedListItem
                 ): Boolean {
                     return oldItem == newItem
                 }
@@ -590,10 +592,10 @@
         }
     }
 
-    private class NullPaddedListViewHolder(
+    private class PlaceholderPaddedListViewHolder(
         context: Context
     ) : RecyclerView.ViewHolder(View(context)) {
-        var boundItem: NullPaddedListItem? = null
+        var boundItem: PlaceholderPaddedListItem? = null
         var boundPos: Int = -1
         override fun toString(): String {
             return "VH[$boundPos , $boundItem]"
@@ -604,16 +606,16 @@
         // top coordinate of the item
         val top: Int,
         // the item it is bound to, unless it was a placeholder
-        val boundItem: NullPaddedListItem?,
+        val boundItem: PlaceholderPaddedListItem?,
         // the position it was bound to
         val boundPos: Int
     )
 
-    private class NullPaddedStorage(
+    private class PlaceholderPaddedStorage(
         override val placeholdersBefore: Int,
-        private val data: List<NullPaddedListItem>,
+        private val data: List<PlaceholderPaddedListItem>,
         override val placeholdersAfter: Int
-    ) : NullPaddedList<NullPaddedListItem> {
+    ) : PlaceholderPaddedList<PlaceholderPaddedListItem> {
         private val stringRepresentation by lazy {
             """
             $placeholdersBefore:${data.size}:$placeholdersAfter
@@ -621,7 +623,7 @@
             """.trimIndent()
         }
 
-        override fun getItem(index: Int): NullPaddedListItem = data[index]
+        override fun getItem(index: Int): PlaceholderPaddedListItem = data[index]
 
         override val size: Int
             get() = placeholdersBefore + data.size + placeholdersAfter
@@ -635,9 +637,9 @@
     private fun createItems(
         startId: Int,
         count: Int
-    ): List<NullPaddedListItem> {
+    ): List<PlaceholderPaddedListItem> {
         return (startId until startId + count).map {
-            NullPaddedListItem(
+            PlaceholderPaddedListItem(
                 id = it,
                 value = "$it"
             )
@@ -650,7 +652,7 @@
     private fun createExpectedSnapshot(
         firstItemTopOffset: Int = 0,
         startItemIndex: Int,
-        backingList: NullPaddedList<NullPaddedListItem>
+        backingList: PlaceholderPaddedList<PlaceholderPaddedListItem>
     ): List<UIItemSnapshot> {
         check(firstItemTopOffset <= 0) {
             "first item offset should not be negative"
@@ -682,8 +684,8 @@
      * it)
      */
     private class ValidatingListUpdateCallback<T>(
-        previousList: NullPaddedList<T>?,
-        private val newList: NullPaddedList<T>
+        previousList: PlaceholderPaddedList<T>?,
+        private val newList: PlaceholderPaddedList<T>
     ) : ListUpdateCallback {
         // used in assertion messages
         val msg = """
@@ -785,8 +787,8 @@
     }
 
     private class TrackingAdapterObserver<T>(
-        previousList: NullPaddedList<T>?,
-        postList: NullPaddedList<T>
+        previousList: PlaceholderPaddedList<T>?,
+        postList: PlaceholderPaddedList<T>
     ) : RecyclerView.AdapterDataObserver() {
         private val callback = ValidatingListUpdateCallback(previousList, postList)
 
@@ -818,7 +820,7 @@
     }
 }
 
-private fun <T> NullPaddedList<T>.get(index: Int): T? {
+private fun <T> PlaceholderPaddedList<T>.get(index: Int): T? {
     if (index < placeholdersBefore) return null
     val storageIndex = index - placeholdersBefore
     if (storageIndex >= dataCount) return null
@@ -828,7 +830,8 @@
 /**
  * Create a snapshot of this current that can be used to verify diffs.
  */
-private fun <T> NullPaddedList<T>.createSnapshot(): MutableList<ListSnapshotItem> = (0 until size)
+private fun <T> PlaceholderPaddedList<T>.createSnapshot():
+    MutableList<ListSnapshotItem> = (0 until size)
     .mapTo(mutableListOf()) { pos ->
         get(pos)?.let {
             ListSnapshotItem.Item(it)
diff --git a/paging/paging-runtime/src/main/java/androidx/paging/AsyncPagedListDiffer.kt b/paging/paging-runtime/src/main/java/androidx/paging/AsyncPagedListDiffer.kt
index 5ed8435..27f5d14 100644
--- a/paging/paging-runtime/src/main/java/androidx/paging/AsyncPagedListDiffer.kt
+++ b/paging/paging-runtime/src/main/java/androidx/paging/AsyncPagedListDiffer.kt
@@ -422,8 +422,8 @@
         val recordingCallback = RecordingCallback()
         pagedList.addWeakCallback(recordingCallback)
         config.backgroundThreadExecutor.execute {
-            val result = oldSnapshot.getNullPaddedList().computeDiff(
-                newSnapshot.getNullPaddedList(),
+            val result = oldSnapshot.getPlaceholderPaddedList().computeDiff(
+                newSnapshot.getPlaceholderPaddedList(),
                 config.diffCallback
             )
 
@@ -446,7 +446,7 @@
     internal fun latchPagedList(
         @Suppress("DEPRECATION") newList: PagedList<T>,
         @Suppress("DEPRECATION") diffSnapshot: PagedList<T>,
-        diffResult: NullPaddedDiffResult,
+        diffResult: PlaceholderPaddedDiffResult,
         recordingCallback: RecordingCallback,
         lastAccessIndex: Int,
         commitCallback: Runnable?
@@ -461,9 +461,9 @@
         snapshot = null
 
         // dispatch updates to UI from previousSnapshot -> newSnapshot
-        previousSnapshot.getNullPaddedList().dispatchDiff(
+        previousSnapshot.getPlaceholderPaddedList().dispatchDiff(
             callback = updateCallback,
-            newList = diffSnapshot.getNullPaddedList(),
+            newList = diffSnapshot.getPlaceholderPaddedList(),
             diffResult = diffResult
         )
 
@@ -481,9 +481,9 @@
             // Note: we don't take into account loads between new list snapshot and new list, but
             // this is only a problem in rare cases when placeholders are disabled, and a load
             // starts (for some reason) and finishes before diff completes.
-            val newPosition = previousSnapshot.getNullPaddedList().transformAnchorIndex(
+            val newPosition = previousSnapshot.getPlaceholderPaddedList().transformAnchorIndex(
                 diffResult,
-                diffSnapshot.getNullPaddedList(),
+                diffSnapshot.getPlaceholderPaddedList(),
                 lastAccessIndex
             )
 
diff --git a/paging/paging-runtime/src/main/java/androidx/paging/AsyncPagingDataDiffer.kt b/paging/paging-runtime/src/main/java/androidx/paging/AsyncPagingDataDiffer.kt
index c0b3e06..b046267 100644
--- a/paging/paging-runtime/src/main/java/androidx/paging/AsyncPagingDataDiffer.kt
+++ b/paging/paging-runtime/src/main/java/androidx/paging/AsyncPagingDataDiffer.kt
@@ -16,6 +16,8 @@
 
 package androidx.paging
 
+import android.os.Handler
+import android.os.Looper
 import androidx.annotation.IntRange
 import androidx.annotation.MainThread
 import androidx.lifecycle.Lifecycle
@@ -23,14 +25,24 @@
 import androidx.paging.LoadType.REFRESH
 import androidx.recyclerview.widget.DiffUtil
 import androidx.recyclerview.widget.ListUpdateCallback
+import java.util.concurrent.CopyOnWriteArrayList
 import java.util.concurrent.atomic.AtomicInteger
+import java.util.concurrent.atomic.AtomicReference
 import kotlin.coroutines.CoroutineContext
 import kotlinx.coroutines.CoroutineDispatcher
 import kotlinx.coroutines.Dispatchers
+import kotlinx.coroutines.channels.Channel.Factory.CONFLATED
 import kotlinx.coroutines.flow.Flow
+import kotlinx.coroutines.flow.MutableStateFlow
+import kotlinx.coroutines.flow.buffer
 import kotlinx.coroutines.flow.filterNotNull
+import kotlinx.coroutines.flow.firstOrNull
+import kotlinx.coroutines.flow.flowOn
+import kotlinx.coroutines.flow.transform
+import kotlinx.coroutines.flow.update
 import kotlinx.coroutines.launch
 import kotlinx.coroutines.withContext
+import kotlinx.coroutines.yield
 
 /**
  * Helper class for mapping a [PagingData] into a
@@ -133,8 +145,7 @@
     )
 
     /** True if we're currently executing [getItem] */
-    @Suppress("MemberVisibilityCanBePrivate") // synthetic access
-    internal var inGetItem: Boolean = false
+    internal val inGetItem = MutableStateFlow(false)
 
     internal val presenter = object : PagingDataPresenter<T>(mainDispatcher) {
         override suspend fun presentPagingDataEvent(event: PagingDataEvent<T>) {
@@ -307,16 +318,6 @@
                 }
             }
         }
-
-        /**
-         * Return if [getItem] is running to post any data modifications.
-         *
-         * This must be done because RecyclerView can't be modified during an onBind, when
-         * [getItem] is generally called.
-         */
-        override fun postEvents(): Boolean {
-            return inGetItem
-        }
     }
 
     private val submitDataId = AtomicInteger(0)
@@ -411,10 +412,10 @@
     @MainThread
     fun getItem(@IntRange(from = 0) index: Int): T? {
         try {
-            inGetItem = true
+            inGetItem.update { true }
             return presenter[index]
         } finally {
-            inGetItem = false
+            inGetItem.update { false }
         }
     }
 
@@ -450,11 +451,22 @@
      * current [PagingData] changes.
      *
      * This flow is conflated, so it buffers the last update to [CombinedLoadStates] and
-     * immediately delivers the current load states on collection.
+     * delivers the current load states on collection when RecyclerView is not dispatching layout.
      *
      * @sample androidx.paging.samples.loadStateFlowSample
      */
-    val loadStateFlow: Flow<CombinedLoadStates> = presenter.loadStateFlow.filterNotNull()
+    val loadStateFlow: Flow<CombinedLoadStates> = presenter.loadStateFlow
+        .filterNotNull()
+        .buffer(CONFLATED)
+        .transform { it ->
+            if (inGetItem.value) {
+                yield()
+                inGetItem.firstOrNull { isGettingItem ->
+                    !isGettingItem
+                }
+            }
+            emit(it)
+        }.flowOn(Dispatchers.Main)
 
     /**
      * A hot [Flow] that emits after the pages presented to the UI are updated, even if the
@@ -507,6 +519,22 @@
     }
 
     /**
+     * The loadStateListener registered internally with [PagingDataPresenter.addLoadStateListener]
+     * when there are [childLoadStateListeners].
+     *
+     * LoadStateUpdates are dispatched to this single internal listener, which will further
+     * dispatch the loadState to [childLoadStateListeners] when [inGetItem] is false.
+     */
+    private val parentLoadStateListener: AtomicReference<((CombinedLoadStates) -> Unit)?> =
+        AtomicReference(null)
+
+    /**
+     * Stores the list of listeners added through [addLoadStateListener]. Invoked
+     * when inGetItem is false.
+     */
+    private val childLoadStateListeners = CopyOnWriteArrayList<(CombinedLoadStates) -> Unit>()
+
+    /**
      * Add a [CombinedLoadStates] listener to observe the loading state of the current [PagingData].
      *
      * As new [PagingData] generations are submitted and displayed, the listener will be notified to
@@ -519,7 +547,10 @@
      * @sample androidx.paging.samples.addLoadStateListenerSample
      */
     fun addLoadStateListener(listener: (CombinedLoadStates) -> Unit) {
-        presenter.addLoadStateListener(listener)
+        if (parentLoadStateListener.get() == null) {
+            addLoadStateListenerInternal(internalLoadStateListener)
+        }
+        childLoadStateListeners.add(listener)
     }
 
     /**
@@ -529,6 +560,42 @@
      * @see addLoadStateListener
      */
     fun removeLoadStateListener(listener: (CombinedLoadStates) -> Unit) {
-        presenter.removeLoadStateListener(listener)
+        childLoadStateListeners.remove(listener)
+        if (childLoadStateListeners.isEmpty()) {
+            val parent = parentLoadStateListener.get()
+            parent?.let { presenter.removeLoadStateListener(it) }
+        }
+    }
+
+    internal fun addLoadStateListenerInternal(listener: (CombinedLoadStates) -> Unit) {
+        parentLoadStateListener.set(listener)
+        presenter.addLoadStateListener(listener)
+    }
+
+    internal val internalLoadStateListener: (CombinedLoadStates) -> Unit = { loadState ->
+        if (!inGetItem.value) {
+            childLoadStateListeners.forEach { it(loadState) }
+        } else {
+            LoadStateListenerHandler.apply {
+                // we only want to send the latest LoadState
+                removeCallbacks(LoadStateListenerRunnable)
+                // enqueue child listeners
+                LoadStateListenerRunnable.loadState.set(loadState)
+                post(LoadStateListenerRunnable)
+            }
+        }
+    }
+
+    private val LoadStateListenerHandler by lazy { Handler(Looper.getMainLooper()) }
+
+    private val LoadStateListenerRunnable = object : Runnable {
+        var loadState = AtomicReference<CombinedLoadStates>(null)
+
+        override fun run() {
+            loadState.get()
+                ?.let { state ->
+                    childLoadStateListeners.forEach { it(state) }
+                }
+        }
     }
 }
diff --git a/paging/paging-runtime/src/main/java/androidx/paging/NullPaddedDiffing.md b/paging/paging-runtime/src/main/java/androidx/paging/PlaceholderPaddedDiffing.md
similarity index 100%
rename from paging/paging-runtime/src/main/java/androidx/paging/NullPaddedDiffing.md
rename to paging/paging-runtime/src/main/java/androidx/paging/PlaceholderPaddedDiffing.md
diff --git a/paging/paging-runtime/src/main/java/androidx/paging/NullPaddedListDiffHelper.kt b/paging/paging-runtime/src/main/java/androidx/paging/PlaceholderPaddedListDiffHelper.kt
similarity index 95%
rename from paging/paging-runtime/src/main/java/androidx/paging/NullPaddedListDiffHelper.kt
rename to paging/paging-runtime/src/main/java/androidx/paging/PlaceholderPaddedListDiffHelper.kt
index bc332f1..7f0ce24 100644
--- a/paging/paging-runtime/src/main/java/androidx/paging/NullPaddedListDiffHelper.kt
+++ b/paging/paging-runtime/src/main/java/androidx/paging/PlaceholderPaddedListDiffHelper.kt
@@ -35,10 +35,10 @@
  * To only inform DiffUtil about single loaded page in this case, by pruning all other nulls from
  * consideration.
  */
-internal fun <T : Any> NullPaddedList<T>.computeDiff(
-    newList: NullPaddedList<T>,
+internal fun <T : Any> PlaceholderPaddedList<T>.computeDiff(
+    newList: PlaceholderPaddedList<T>,
     diffCallback: DiffUtil.ItemCallback<T>
-): NullPaddedDiffResult {
+): PlaceholderPaddedDiffResult {
     val oldSize = dataCount
     val newSize = newList.dataCount
 
@@ -84,23 +84,23 @@
     val hasOverlap = (0 until dataCount).any {
         diffResult.convertOldPositionToNew(it) != RecyclerView.NO_POSITION
     }
-    return NullPaddedDiffResult(
+    return PlaceholderPaddedDiffResult(
         diff = diffResult,
         hasOverlap = hasOverlap
     )
 }
 
 /**
- * See NullPaddedDiffing.md for how this works and why it works that way :).
+ * See PlaceholderPaddedDiffing.md for how this works and why it works that way :).
  *
  * Note: if lists mutate between diffing the snapshot and dispatching the diff here, then we
  * handle this by passing the snapshot to the callback, and dispatching those changes
  * immediately after dispatching this diff.
  */
-internal fun <T : Any> NullPaddedList<T>.dispatchDiff(
+internal fun <T : Any> PlaceholderPaddedList<T>.dispatchDiff(
     callback: ListUpdateCallback,
-    newList: NullPaddedList<T>,
-    diffResult: NullPaddedDiffResult
+    newList: PlaceholderPaddedList<T>,
+    diffResult: PlaceholderPaddedDiffResult
 ) {
     if (diffResult.hasOverlap) {
         OverlappingListsDiffDispatcher.dispatchDiff(
@@ -125,9 +125,9 @@
  * Given an oldPosition representing an anchor in the old data set, computes its new position
  * after the diff, or a guess if it no longer exists.
  */
-internal fun NullPaddedList<*>.transformAnchorIndex(
-    diffResult: NullPaddedDiffResult,
-    newList: NullPaddedList<*>,
+internal fun PlaceholderPaddedList<*>.transformAnchorIndex(
+    diffResult: PlaceholderPaddedDiffResult,
+    newList: PlaceholderPaddedList<*>,
     oldPosition: Int
 ): Int {
     if (!diffResult.hasOverlap) {
@@ -163,21 +163,21 @@
     return oldPosition.coerceIn(0 until newList.size)
 }
 
-internal class NullPaddedDiffResult(
+internal class PlaceholderPaddedDiffResult(
     val diff: DiffUtil.DiffResult,
     // true if two lists have at least 1 item the same
     val hasOverlap: Boolean
 )
 
 /**
- * Helper class to implement the heuristic documented in NullPaddedDiffing.md.
+ * Helper class to implement the heuristic documented in PlaceholderPaddedDiffing.md.
  */
 internal object OverlappingListsDiffDispatcher {
     fun <T> dispatchDiff(
-        oldList: NullPaddedList<T>,
-        newList: NullPaddedList<T>,
+        oldList: PlaceholderPaddedList<T>,
+        newList: PlaceholderPaddedList<T>,
         callback: ListUpdateCallback,
-        diffResult: NullPaddedDiffResult
+        diffResult: PlaceholderPaddedDiffResult
     ) {
         val callbackWrapper = PlaceholderUsingUpdateCallback(
             oldList = oldList,
@@ -190,8 +190,8 @@
 
     @Suppress("NOTHING_TO_INLINE")
     private class PlaceholderUsingUpdateCallback<T>(
-        private val oldList: NullPaddedList<T>,
-        private val newList: NullPaddedList<T>,
+        private val oldList: PlaceholderPaddedList<T>,
+        private val newList: PlaceholderPaddedList<T>,
         private val callback: ListUpdateCallback
     ) : ListUpdateCallback {
         // These variables hold the "current" value for placeholders and storage count and are
@@ -464,8 +464,8 @@
 internal object DistinctListsDiffDispatcher {
     fun <T : Any> dispatchDiff(
         callback: ListUpdateCallback,
-        oldList: NullPaddedList<T>,
-        newList: NullPaddedList<T>,
+        oldList: PlaceholderPaddedList<T>,
+        newList: PlaceholderPaddedList<T>,
     ) {
         val storageOverlapStart = maxOf(
             oldList.placeholdersBefore, newList.placeholdersBefore
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
index 0a38d7d..b1af845 100644
--- a/paging/paging-testing/src/commonMain/kotlin/androidx/paging/testing/PagerFlowSnapshot.kt
+++ b/paging/paging-testing/src/commonMain/kotlin/androidx/paging/testing/PagerFlowSnapshot.kt
@@ -25,6 +25,7 @@
 import androidx.paging.PagingData
 import androidx.paging.PagingDataEvent
 import androidx.paging.PagingDataPresenter
+import androidx.paging.awaitNotLoading
 import androidx.paging.testing.ErrorRecovery.RETRY
 import androidx.paging.testing.ErrorRecovery.RETURN_CURRENT_SNAPSHOT
 import androidx.paging.testing.ErrorRecovery.THROW
@@ -36,10 +37,7 @@
 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
 
 /**
@@ -188,11 +186,9 @@
 internal suspend fun <Value : Any> CompletablePagingDataPresenter<Value>.awaitNotLoading(
     errorHandler: LoadErrorHandler
 ) {
-    val state = completableLoadStateFlow.filterNotNull().debounce(1).filter {
-        it.isIdle() || it.hasError()
-    }.firstOrNull()
+    val state = completableLoadStateFlow.filterNotNull().awaitNotLoading()
 
-    if (state != null && state.hasError()) {
+    if (state != null && state.hasError) {
         handleLoadError(state, errorHandler)
     }
 }
@@ -210,26 +206,6 @@
 }
 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
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
index d425c20..ecb52eb 100644
--- a/paging/paging-testing/src/commonMain/kotlin/androidx/paging/testing/SnapshotLoader.kt
+++ b/paging/paging-testing/src/commonMain/kotlin/androidx/paging/testing/SnapshotLoader.kt
@@ -390,7 +390,7 @@
      * Computes the offset to add to the index when loading items from presenter.
      *
      * 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
+     * with placeholders disabled. For example, loaded items(10-12) in the PlaceholderPaddedList
      * would have item(12) at presenter[2]. If we prefetched items(7-9), item(12) would now be in
      * presenter[5].
      *
diff --git a/playground-common/androidx-shared.properties b/playground-common/androidx-shared.properties
index c4cb7e1..5b6eb52 100644
--- a/playground-common/androidx-shared.properties
+++ b/playground-common/androidx-shared.properties
@@ -24,7 +24,7 @@
 # This separation is necessary to ensure gradle can read certain properties
 # at configuration time.
 
-org.gradle.jvmargs=-Xmx4g -XX:+HeapDumpOnOutOfMemoryError -XX:+UseParallelGC -XX:MaxMetaspaceSize=512m -Dkotlin.daemon.jvm.options=-XX:MaxMetaspaceSize=1g -Dlint.nullness.ignore-deprecated=true
+org.gradle.jvmargs=-Xmx6g -XX:+HeapDumpOnOutOfMemoryError -XX:+UseParallelGC -XX:MaxMetaspaceSize=512m -Dkotlin.daemon.jvm.options=-XX:MaxMetaspaceSize=1g -Dlint.nullness.ignore-deprecated=true  -Dlint.nullness.ignore-deprecated=true
 org.gradle.configureondemand=true
 org.gradle.parallel=true
 org.gradle.caching=true
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 a86db9e..d95d88a 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
@@ -110,6 +110,7 @@
                 /* minAdServicesVersion=*/ 10,
                 /* minExtServicesVersion=*/ 10))
 
+        mockCustomAudienceManager(mContext, mValidAdExtServicesSdkExtVersion)
         val managerCompat = from(mContext)
 
         // Actually invoke the compat code.
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 fbfde14..e55c368 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
@@ -156,6 +156,7 @@
         Assume.assumeTrue("maxSdkVersion = API 31-34 ext 9",
             AdServicesInfo.adServicesVersion() < 10 && AdServicesInfo.extServicesVersion() < 10)
 
+        mockAdSelectionManager(mContext, mValidAdExtServicesSdkExtVersion)
         val managerCompat = obtain(mContext)
         val getAdSelectionDataRequest = GetAdSelectionDataRequest(seller)
         // Verify that it throws an exception
@@ -177,6 +178,7 @@
         Assume.assumeTrue("maxSdkVersion = API 31-34 ext 9",
             AdServicesInfo.adServicesVersion() < 10 && AdServicesInfo.extServicesVersion() < 10)
 
+        mockAdSelectionManager(mContext, mValidAdExtServicesSdkExtVersion)
         val managerCompat = obtain(mContext)
         val persistAdSelectionResultRequest = PersistAdSelectionResultRequest(
             adSelectionId,
@@ -202,6 +204,7 @@
         Assume.assumeTrue("maxSdkVersion = API 31-34 ext 9",
             AdServicesInfo.adServicesVersion() < 10 && AdServicesInfo.extServicesVersion() < 10)
 
+        mockAdSelectionManager(mContext, mValidAdExtServicesSdkExtVersion)
         val managerCompat = obtain(mContext)
         val reportImpressionRequest = ReportImpressionRequest(adSelectionId)
 
@@ -225,6 +228,7 @@
         Assume.assumeTrue("maxSdkVersion = API 31-34 ext 9",
             AdServicesInfo.adServicesVersion() < 10 && AdServicesInfo.extServicesVersion() < 10)
 
+        mockAdSelectionManager(mContext, mValidAdExtServicesSdkExtVersion)
         val managerCompat = obtain(mContext)
         // Verify that it throws an exception
         assertThrows(UnsupportedOperationException::class.java) {
diff --git a/room/integration-tests/kotlintestapp/src/androidTest/java/androidx/room/integration/kotlintestapp/migration/AutoMigrationTest.kt b/room/integration-tests/kotlintestapp/src/androidTest/java/androidx/room/integration/kotlintestapp/migration/AutoMigrationTest.kt
index 7e2e54b..16aef34 100644
--- a/room/integration-tests/kotlintestapp/src/androidTest/java/androidx/room/integration/kotlintestapp/migration/AutoMigrationTest.kt
+++ b/room/integration-tests/kotlintestapp/src/androidTest/java/androidx/room/integration/kotlintestapp/migration/AutoMigrationTest.kt
@@ -50,6 +50,7 @@
     }
 
     @Test
+    @Suppress("DEPRECATION") // Due to TableInfo.read()
     fun goFromV1ToV2() {
         createFirstVersion()
         val db = helper.runMigrationsAndValidate(
@@ -83,6 +84,7 @@
     }
 
     @Test
+    @Suppress("DEPRECATION") // Due to TableInfo.read()
     fun testAutoMigrationWithNewEmbeddedField() {
         val embeddedHelper = MigrationTestHelper(
             InstrumentationRegistry.getInstrumentation(),
diff --git a/room/integration-tests/kotlintestapp/src/androidTest/java/androidx/room/integration/kotlintestapp/migration/MigrationKotlinTest.kt b/room/integration-tests/kotlintestapp/src/androidTest/java/androidx/room/integration/kotlintestapp/migration/MigrationKotlinTest.kt
index 625d44f..96450ac 100644
--- a/room/integration-tests/kotlintestapp/src/androidTest/java/androidx/room/integration/kotlintestapp/migration/MigrationKotlinTest.kt
+++ b/room/integration-tests/kotlintestapp/src/androidTest/java/androidx/room/integration/kotlintestapp/migration/MigrationKotlinTest.kt
@@ -174,6 +174,7 @@
 
     @Test
     @Throws(IOException::class)
+    @Suppress("DEPRECATION") // Due to TableInfo.read()
     fun removeColumn() {
         helper.createDatabase(TEST_DB, 4)
         val db = helper.runMigrationsAndValidate(
@@ -186,6 +187,7 @@
 
     @Test
     @Throws(IOException::class)
+    @Suppress("DEPRECATION") // Due to TableInfo.read()
     fun dropTable() {
         helper.createDatabase(TEST_DB, 5)
         val db = helper.runMigrationsAndValidate(
@@ -204,6 +206,7 @@
 
     @Test
     @Throws(IOException::class)
+    @Suppress("DEPRECATION") // Due to TableInfo.read()
     fun failedToDropTableDontVerify() {
         helper.createDatabase(TEST_DB, 5)
         val db = helper.runMigrationsAndValidate(
@@ -244,6 +247,7 @@
 
     @Test
     @Throws(IOException::class)
+    @Suppress("DEPRECATION") // Due to TableInfo.read()
     fun newTableWithForeignKey() {
         helper.createDatabase(TEST_DB, 6)
         val db = helper.runMigrationsAndValidate(
diff --git a/room/room-compiler-processing-testing/build.gradle b/room/room-compiler-processing-testing/build.gradle
index 4e2e6ab..f3b5539 100644
--- a/room/room-compiler-processing-testing/build.gradle
+++ b/room/room-compiler-processing-testing/build.gradle
@@ -80,6 +80,16 @@
     }
 }
 
+afterEvaluate {
+    lint {
+        lintOptions {
+            // Until fully switch to K2, existing FE1.0 usages are legitimate
+            // TODO(b/314151707)
+            disable("KotlincFE10")
+        }
+    }
+}
+
 androidx {
     name = "Room XProcessor Testing"
     type = LibraryType.ANNOTATION_PROCESSOR_UTILS
diff --git a/room/room-compiler/src/main/kotlin/androidx/room/ext/xpoet_ext.kt b/room/room-compiler/src/main/kotlin/androidx/room/ext/xpoet_ext.kt
index 0856f18..dc9a2b9 100644
--- a/room/room-compiler/src/main/kotlin/androidx/room/ext/xpoet_ext.kt
+++ b/room/room-compiler/src/main/kotlin/androidx/room/ext/xpoet_ext.kt
@@ -79,6 +79,7 @@
     val ROOM_OPEN_DELEGATE = XClassName.get(ROOM_PACKAGE, "RoomOpenDelegate")
     val ROOM_OPEN_DELEGATE_VALIDATION_RESULT =
         XClassName.get(ROOM_PACKAGE, "RoomOpenDelegate", "ValidationResult")
+    val STATEMENT_UTIL = XClassName.get("$ROOM_PACKAGE.util", "SQLiteStatementUtil")
 }
 
 object RoomAnnotationTypeNames {
diff --git a/room/room-compiler/src/main/kotlin/androidx/room/processor/DatabaseProcessor.kt b/room/room-compiler/src/main/kotlin/androidx/room/processor/DatabaseProcessor.kt
index 6fbea1d..be5eb7b 100644
--- a/room/room-compiler/src/main/kotlin/androidx/room/processor/DatabaseProcessor.kt
+++ b/room/room-compiler/src/main/kotlin/androidx/room/processor/DatabaseProcessor.kt
@@ -22,7 +22,6 @@
 import androidx.room.compiler.codegen.XTypeName
 import androidx.room.compiler.processing.XAnnotationBox
 import androidx.room.compiler.processing.XElement
-import androidx.room.compiler.processing.XType
 import androidx.room.compiler.processing.XTypeElement
 import androidx.room.ext.RoomTypeNames
 import androidx.room.migration.bundle.DatabaseBundle
@@ -50,8 +49,8 @@
 class DatabaseProcessor(baseContext: Context, val element: XTypeElement) {
     val context = baseContext.fork(element)
 
-    val roomDatabaseType: XType by lazy {
-        context.processingEnv.requireType(RoomTypeNames.ROOM_DB)
+    private val roomDatabaseTypeElement: XTypeElement by lazy {
+        context.processingEnv.requireTypeElement(RoomTypeNames.ROOM_DB)
     }
 
     fun process(): Database {
@@ -70,7 +69,7 @@
         validateForeignKeys(element, entities)
         validateExternalContentFts(element, entities)
 
-        val extendsRoomDb = roomDatabaseType.isAssignableFrom(element.type)
+        val extendsRoomDb = roomDatabaseTypeElement.type.isAssignableFrom(element.type)
         context.checker.check(extendsRoomDb, element, ProcessorErrors.DB_MUST_EXTEND_ROOM_DB)
 
         val views = resolveDatabaseViews(viewsMap.values.toList())
@@ -128,6 +127,9 @@
 
         val hasForeignKeys = entities.any { it.foreignKeys.isNotEmpty() }
 
+        val hasClearAllTables = roomDatabaseTypeElement.getDeclaredMethods()
+            .any { it.name == "clearAllTables" }
+
         context.checker.check(
             predicate = dbAnnotation.value.version > 0,
             element = element,
@@ -142,7 +144,8 @@
             views = views,
             daoMethods = daoMethods,
             exportSchema = dbAnnotation.value.exportSchema,
-            enableForeignKeys = hasForeignKeys
+            enableForeignKeys = hasForeignKeys,
+            overrideClearAllTables = hasClearAllTables,
         )
         database.autoMigrations = processAutoMigrations(element, database.bundle)
         return database
diff --git a/room/room-compiler/src/main/kotlin/androidx/room/solver/CodeGenScope.kt b/room/room-compiler/src/main/kotlin/androidx/room/solver/CodeGenScope.kt
index a4e230b..e552cd7 100644
--- a/room/room-compiler/src/main/kotlin/androidx/room/solver/CodeGenScope.kt
+++ b/room/room-compiler/src/main/kotlin/androidx/room/solver/CodeGenScope.kt
@@ -24,7 +24,9 @@
  * Defines a code generation scope where we can provide temporary variables, global variables etc
  */
 class CodeGenScope(
-    val writer: TypeWriter
+    val writer: TypeWriter,
+    // TODO(b/319660042): Remove once migration to driver API is done.
+    val useDriverApi: Boolean = false
 ) {
     val language = writer.codeLanguage
     val builder by lazy { XCodeBlock.builder(language) }
@@ -63,7 +65,7 @@
      * Copies all variable indices but excludes generated code.
      */
     fun fork(): CodeGenScope {
-        val forked = CodeGenScope(writer)
+        val forked = CodeGenScope(writer, useDriverApi)
         forked.tmpVarIndices.putAll(tmpVarIndices)
         return forked
     }
diff --git a/room/room-compiler/src/main/kotlin/androidx/room/solver/query/result/InstantQueryResultBinder.kt b/room/room-compiler/src/main/kotlin/androidx/room/solver/query/result/InstantQueryResultBinder.kt
index 095a6dd..67e2337 100644
--- a/room/room-compiler/src/main/kotlin/androidx/room/solver/query/result/InstantQueryResultBinder.kt
+++ b/room/room-compiler/src/main/kotlin/androidx/room/solver/query/result/InstantQueryResultBinder.kt
@@ -15,11 +15,17 @@
  */
 package androidx.room.solver.query.result
 
+import androidx.room.compiler.codegen.CodeLanguage
 import androidx.room.compiler.codegen.XCodeBlock
+import androidx.room.compiler.codegen.XFunSpec.Builder.Companion.addStatement
 import androidx.room.compiler.codegen.XMemberName.Companion.packageMember
 import androidx.room.compiler.codegen.XPropertySpec
+import androidx.room.compiler.codegen.XTypeName
+import androidx.room.compiler.codegen.box
 import androidx.room.ext.AndroidTypeNames
+import androidx.room.ext.Function1TypeSpec
 import androidx.room.ext.RoomTypeNames
+import androidx.room.ext.SQLiteDriverTypeNames
 import androidx.room.solver.CodeGenScope
 
 /**
@@ -74,4 +80,88 @@
         }
         transactionWrapper?.endTransactionWithControlFlow()
     }
+
+    override fun isMigratedToDriver() = adapter?.isMigratedToDriver() == true
+
+    override fun convertAndReturn(
+        sqlQueryVar: String,
+        dbProperty: XPropertySpec,
+        bindStatement: CodeGenScope.(String) -> Unit,
+        returnTypeName: XTypeName,
+        inTransaction: Boolean,
+        scope: CodeGenScope
+    ) {
+        when (scope.language) {
+            CodeLanguage.JAVA -> convertAndReturnJava(
+                sqlQueryVar, dbProperty, bindStatement, returnTypeName, inTransaction, scope
+            )
+            CodeLanguage.KOTLIN -> convertAndReturnKotlin(
+                sqlQueryVar, dbProperty, bindStatement, inTransaction, scope
+            )
+        }
+    }
+
+    private fun convertAndReturnJava(
+        sqlQueryVar: String,
+        dbProperty: XPropertySpec,
+        bindStatement: CodeGenScope.(String) -> Unit,
+        returnTypeName: XTypeName,
+        inTransaction: Boolean,
+        scope: CodeGenScope
+    ) {
+        val performFunctionName = getPerformFunctionName(inTransaction)
+        val statementVar = scope.getTmpVar("_stmt")
+        scope.builder.addStatement(
+            "return %M(%N, %L, %L)",
+            RoomTypeNames.DB_UTIL.packageMember(performFunctionName),
+            dbProperty,
+            sqlQueryVar,
+            // TODO(b/322387497): Generate lambda syntax if possible
+            Function1TypeSpec(
+                language = scope.language,
+                parameterTypeName = SQLiteDriverTypeNames.STATEMENT,
+                parameterName = statementVar,
+                returnTypeName = returnTypeName.box()
+            ) {
+                val functionScope = scope.fork()
+                bindStatement(functionScope, statementVar)
+                val outVar = functionScope.getTmpVar("_result")
+                adapter?.convert(outVar, statementVar, functionScope)
+                this.addCode(functionScope.generate())
+                this.addStatement("return %L", outVar)
+            }
+        )
+    }
+
+    private fun convertAndReturnKotlin(
+        sqlQueryVar: String,
+        dbProperty: XPropertySpec,
+        bindStatement: CodeGenScope.(String) -> Unit,
+        inTransaction: Boolean,
+        scope: CodeGenScope
+    ) {
+        val statementVar = scope.getTmpVar("_stmt")
+        val performFunctionName = getPerformFunctionName(inTransaction)
+        scope.builder.apply {
+            beginControlFlow(
+                "return %M(%N, %L) { %L ->",
+                RoomTypeNames.DB_UTIL.packageMember(performFunctionName),
+                dbProperty,
+                sqlQueryVar,
+                statementVar
+            )
+            bindStatement(scope, statementVar)
+            val outVar = scope.getTmpVar("_result")
+            adapter?.convert(outVar, statementVar, scope)
+            addStatement("%L", outVar)
+            endControlFlow()
+        }
+    }
+
+    private fun getPerformFunctionName(inTransaction: Boolean) =
+        if (inTransaction) {
+            "performReadTransactionBlocking"
+        } else {
+            "performReadBlocking"
+        }
 }
diff --git a/room/room-compiler/src/main/kotlin/androidx/room/solver/query/result/PojoIndexAdapter.kt b/room/room-compiler/src/main/kotlin/androidx/room/solver/query/result/PojoIndexAdapter.kt
index e868677..dde8f15 100644
--- a/room/room-compiler/src/main/kotlin/androidx/room/solver/query/result/PojoIndexAdapter.kt
+++ b/room/room-compiler/src/main/kotlin/androidx/room/solver/query/result/PojoIndexAdapter.kt
@@ -21,6 +21,7 @@
 import androidx.room.compiler.codegen.XMemberName.Companion.packageMember
 import androidx.room.compiler.codegen.XTypeName
 import androidx.room.ext.RoomTypeNames.CURSOR_UTIL
+import androidx.room.ext.RoomTypeNames.STATEMENT_UTIL
 import androidx.room.ext.capitalize
 import androidx.room.ext.stripNonJava
 import androidx.room.parser.ParsedQuery
@@ -70,13 +71,18 @@
                 } else {
                     "getColumnIndexOrThrow"
                 }
+                val packageMember = if (scope.useDriverApi) {
+                    STATEMENT_UTIL.packageMember(indexMethod)
+                } else {
+                    CURSOR_UTIL.packageMember(indexMethod)
+                }
                 scope.builder.addLocalVariable(
                     name = indexVar,
                     typeName = XTypeName.PRIMITIVE_INT,
                     assignExpr = XCodeBlock.of(
                         scope.language,
                         "%M(%L, %S)",
-                        CURSOR_UTIL.packageMember(indexMethod),
+                        packageMember,
                         cursorVarName,
                         it.columnName
 
diff --git a/room/room-compiler/src/main/kotlin/androidx/room/solver/query/result/PojoRowAdapter.kt b/room/room-compiler/src/main/kotlin/androidx/room/solver/query/result/PojoRowAdapter.kt
index 570c1f9..28f9d56 100644
--- a/room/room-compiler/src/main/kotlin/androidx/room/solver/query/result/PojoRowAdapter.kt
+++ b/room/room-compiler/src/main/kotlin/androidx/room/solver/query/result/PojoRowAdapter.kt
@@ -149,6 +149,10 @@
 
     override fun getDefaultIndexAdapter() = indexAdapter
 
+    override fun isMigratedToDriver(): Boolean {
+        return relationCollectors.isEmpty()
+    }
+
     data class PojoMapping(
         val pojo: Pojo,
         val matchedFields: List<Field>,
diff --git a/room/room-compiler/src/main/kotlin/androidx/room/solver/query/result/QueryResultAdapter.kt b/room/room-compiler/src/main/kotlin/androidx/room/solver/query/result/QueryResultAdapter.kt
index f6cb819..c11dc17 100644
--- a/room/room-compiler/src/main/kotlin/androidx/room/solver/query/result/QueryResultAdapter.kt
+++ b/room/room-compiler/src/main/kotlin/androidx/room/solver/query/result/QueryResultAdapter.kt
@@ -37,4 +37,7 @@
     // (e.g. does done to satisfy @Relation fields).
     fun accessedTableNames(): List<String> =
         rowAdapters.filterIsInstance<PojoRowAdapter>().flatMap { it.relationTableNames() }
+
+    // TODO(b/319660042): Remove once migration to driver API is done.
+    open fun isMigratedToDriver(): Boolean = false
 }
diff --git a/room/room-compiler/src/main/kotlin/androidx/room/solver/query/result/QueryResultBinder.kt b/room/room-compiler/src/main/kotlin/androidx/room/solver/query/result/QueryResultBinder.kt
index f2fda6b..83c3c83 100644
--- a/room/room-compiler/src/main/kotlin/androidx/room/solver/query/result/QueryResultBinder.kt
+++ b/room/room-compiler/src/main/kotlin/androidx/room/solver/query/result/QueryResultBinder.kt
@@ -17,6 +17,7 @@
 package androidx.room.solver.query.result
 
 import androidx.room.compiler.codegen.XPropertySpec
+import androidx.room.compiler.codegen.XTypeName
 import androidx.room.solver.CodeGenScope
 
 /**
@@ -38,4 +39,22 @@
         inTransaction: Boolean,
         scope: CodeGenScope
     )
+
+    // TODO(b/319660042): Remove once migration to driver API is done.
+    open fun isMigratedToDriver(): Boolean = false
+
+    /**
+     * Receives the SQL and a function to bind args into a statement, it must then generate the
+     * code that steps on the query, reads its columns and returns the result.
+     */
+    open fun convertAndReturn(
+        sqlQueryVar: String,
+        dbProperty: XPropertySpec,
+        bindStatement: CodeGenScope.(String) -> Unit,
+        returnTypeName: XTypeName,
+        inTransaction: Boolean,
+        scope: CodeGenScope
+    ) {
+        error("Result binder has not been migrated to use driver API.")
+    }
 }
diff --git a/room/room-compiler/src/main/kotlin/androidx/room/solver/query/result/RowAdapter.kt b/room/room-compiler/src/main/kotlin/androidx/room/solver/query/result/RowAdapter.kt
index be467f2..4d3de1b 100644
--- a/room/room-compiler/src/main/kotlin/androidx/room/solver/query/result/RowAdapter.kt
+++ b/room/room-compiler/src/main/kotlin/androidx/room/solver/query/result/RowAdapter.kt
@@ -51,4 +51,7 @@
      * Gets the default index adapter for the implementation
      */
     abstract fun getDefaultIndexAdapter(): IndexAdapter
+
+    // TODO(b/319660042): Remove once migration to driver API is done.
+    open fun isMigratedToDriver(): Boolean = false
 }
diff --git a/room/room-compiler/src/main/kotlin/androidx/room/solver/query/result/SingleItemQueryResultAdapter.kt b/room/room-compiler/src/main/kotlin/androidx/room/solver/query/result/SingleItemQueryResultAdapter.kt
index 519ff4a..c102404 100644
--- a/room/room-compiler/src/main/kotlin/androidx/room/solver/query/result/SingleItemQueryResultAdapter.kt
+++ b/room/room-compiler/src/main/kotlin/androidx/room/solver/query/result/SingleItemQueryResultAdapter.kt
@@ -31,7 +31,8 @@
         scope.builder.apply {
             rowAdapter.onCursorReady(cursorVarName = cursorVarName, scope = scope)
             addLocalVariable(outVarName, type.asTypeName())
-            beginControlFlow("if (%L.moveToFirst())", cursorVarName).apply {
+            val stepName = if (scope.useDriverApi) "step" else "moveToFirst"
+            beginControlFlow("if (%L.$stepName())", cursorVarName).apply {
                 rowAdapter.convert(outVarName, cursorVarName, scope)
             }
             nextControlFlow("else").apply {
@@ -53,4 +54,6 @@
             endControlFlow()
         }
     }
+
+    override fun isMigratedToDriver(): Boolean = rowAdapter.isMigratedToDriver()
 }
diff --git a/room/room-compiler/src/main/kotlin/androidx/room/solver/types/EnumColumnTypeAdapter.kt b/room/room-compiler/src/main/kotlin/androidx/room/solver/types/EnumColumnTypeAdapter.kt
index 7255e20..7ed2495 100644
--- a/room/room-compiler/src/main/kotlin/androidx/room/solver/types/EnumColumnTypeAdapter.kt
+++ b/room/room-compiler/src/main/kotlin/androidx/room/solver/types/EnumColumnTypeAdapter.kt
@@ -44,9 +44,10 @@
     ) {
         val stringToEnumMethod = stringToEnumMethod(scope)
         scope.builder.apply {
+            val getter = if (scope.useDriverApi) "getText" else "getString"
             fun XCodeBlock.Builder.addGetStringStatement() {
                 addStatement(
-                    "%L = %N(%L.getString(%L))",
+                    "%L = %N(%L.$getter(%L))",
                     outVarName,
                     stringToEnumMethod,
                     cursorVarName,
@@ -72,10 +73,11 @@
         scope: CodeGenScope
     ) {
         val enumToStringMethod = enumToStringMethod(scope)
+        val setter = if (scope.useDriverApi) "bindText" else "bindString"
         scope.builder.apply {
             fun XCodeBlock.Builder.addBindStringStatement() {
                 addStatement(
-                    "%L.bindString(%L, %N(%L))",
+                    "%L.$setter(%L, %N(%L))",
                     stmtName, indexVarName, enumToStringMethod, valueVarName,
                 )
             }
diff --git a/room/room-compiler/src/main/kotlin/androidx/room/solver/types/PrimitiveColumnTypeAdapter.kt b/room/room-compiler/src/main/kotlin/androidx/room/solver/types/PrimitiveColumnTypeAdapter.kt
index d1fe631..25ceb7b 100644
--- a/room/room-compiler/src/main/kotlin/androidx/room/solver/types/PrimitiveColumnTypeAdapter.kt
+++ b/room/room-compiler/src/main/kotlin/androidx/room/solver/types/PrimitiveColumnTypeAdapter.kt
@@ -45,15 +45,16 @@
         enum class Primitive(
             val typeName: XTypeName,
             val cursorGetter: String,
+            val stmtGetter: String,
             val stmtSetter: String,
         ) {
-            INT(PRIMITIVE_INT, "getInt", "bindLong"),
-            SHORT(PRIMITIVE_SHORT, "getShort", "bindLong"),
-            BYTE(PRIMITIVE_BYTE, "getShort", "bindLong"),
-            LONG(PRIMITIVE_LONG, "getLong", "bindLong"),
-            CHAR(PRIMITIVE_CHAR, "getInt", "bindLong"),
-            FLOAT(PRIMITIVE_FLOAT, "getFloat", "bindDouble"),
-            DOUBLE(PRIMITIVE_DOUBLE, "getDouble", "bindDouble"),
+            INT(PRIMITIVE_INT, "getInt", "getLong", "bindLong"),
+            SHORT(PRIMITIVE_SHORT, "getShort", "getLong", "bindLong"),
+            BYTE(PRIMITIVE_BYTE, "getShort", "getLong", "bindLong"),
+            LONG(PRIMITIVE_LONG, "getLong", "getLong", "bindLong"),
+            CHAR(PRIMITIVE_CHAR, "getInt", "getLong", "bindLong"),
+            FLOAT(PRIMITIVE_FLOAT, "getFloat", "getDouble", "bindDouble"),
+            DOUBLE(PRIMITIVE_DOUBLE, "getDouble", "getDouble", "bindDouble"),
         }
 
         private fun getAffinity(primitive: Primitive) = when (primitive) {
@@ -77,6 +78,7 @@
     }
 
     private val cursorGetter = primitive.cursorGetter
+    private val stmtGetter = primitive.stmtGetter
     private val stmtSetter = primitive.stmtSetter
 
     override fun bindToStmt(
@@ -123,14 +125,25 @@
                 scope.language,
                 "%L.%L(%L)",
                 cursorVarName,
-                cursorGetter,
+                if (scope.useDriverApi) stmtGetter else cursorGetter,
                 indexVarName
             ).let {
-                // These primitives don't have an exact cursor getter.
-                val castFunction = when (primitive) {
-                    Primitive.BYTE -> "toByte"
-                    Primitive.CHAR -> "toChar"
-                    else -> null
+                // These primitives don't have an exact cursor / statement getter.
+                val castFunction = if (scope.useDriverApi) {
+                    when (primitive) {
+                        Primitive.INT -> "toInt"
+                        Primitive.SHORT -> "toShort"
+                        Primitive.BYTE -> "toByte"
+                        Primitive.CHAR -> "toChar"
+                        Primitive.FLOAT -> "toFloat"
+                        else -> null
+                    }
+                } else {
+                    when (primitive) {
+                        Primitive.BYTE -> "toByte"
+                        Primitive.CHAR -> "toChar"
+                        else -> null
+                    }
                 } ?: return@let it
                 when (it.language) {
                     // For Java a cast will suffice
diff --git a/room/room-compiler/src/main/kotlin/androidx/room/solver/types/StringColumnTypeAdapter.kt b/room/room-compiler/src/main/kotlin/androidx/room/solver/types/StringColumnTypeAdapter.kt
index c68a9a9..3ce4028 100644
--- a/room/room-compiler/src/main/kotlin/androidx/room/solver/types/StringColumnTypeAdapter.kt
+++ b/room/room-compiler/src/main/kotlin/androidx/room/solver/types/StringColumnTypeAdapter.kt
@@ -32,15 +32,16 @@
         indexVarName: String,
         scope: CodeGenScope
     ) {
+        val getter = if (scope.useDriverApi) "getText" else "getString"
         scope.builder.apply {
             if (out.nullability == XNullability.NONNULL) {
-                addStatement("%L = %L.getString(%L)", outVarName, cursorVarName, indexVarName)
+                addStatement("%L = %L.$getter(%L)", outVarName, cursorVarName, indexVarName)
             } else {
                 beginControlFlow("if (%L.isNull(%L))", cursorVarName, indexVarName).apply {
                     addStatement("%L = null", outVarName)
                 }
                 nextControlFlow("else").apply {
-                    addStatement("%L = %L.getString(%L)", outVarName, cursorVarName, indexVarName)
+                    addStatement("%L = %L.$getter(%L)", outVarName, cursorVarName, indexVarName)
                 }
                 endControlFlow()
             }
@@ -53,14 +54,15 @@
         valueVarName: String,
         scope: CodeGenScope
     ) {
+        val setter = if (scope.useDriverApi) "bindText" else "bindString"
         scope.builder.apply {
             if (out.nullability == XNullability.NONNULL) {
-                addStatement("%L.bindString(%L, %L)", stmtName, indexVarName, valueVarName)
+                addStatement("%L.$setter(%L, %L)", stmtName, indexVarName, valueVarName)
             } else {
                 beginControlFlow("if (%L == null)", valueVarName)
                     .addStatement("%L.bindNull(%L)", stmtName, indexVarName)
                 nextControlFlow("else")
-                    .addStatement("%L.bindString(%L, %L)", stmtName, indexVarName, valueVarName)
+                    .addStatement("%L.$setter(%L, %L)", stmtName, indexVarName, valueVarName)
                 endControlFlow()
             }
         }
diff --git a/room/room-compiler/src/main/kotlin/androidx/room/vo/Database.kt b/room/room-compiler/src/main/kotlin/androidx/room/vo/Database.kt
index ed1476c..04dec4a 100644
--- a/room/room-compiler/src/main/kotlin/androidx/room/vo/Database.kt
+++ b/room/room-compiler/src/main/kotlin/androidx/room/vo/Database.kt
@@ -39,7 +39,8 @@
     val daoMethods: List<DaoMethod>,
     val version: Int,
     val exportSchema: Boolean,
-    val enableForeignKeys: Boolean
+    val enableForeignKeys: Boolean,
+    val overrideClearAllTables: Boolean
 ) {
     // This variable will be set once auto-migrations are processed given the DatabaseBundle from
     // this object. This is necessary for tracking the versions involved in the auto-migration.
diff --git a/room/room-compiler/src/main/kotlin/androidx/room/writer/DaoWriter.kt b/room/room-compiler/src/main/kotlin/androidx/room/writer/DaoWriter.kt
index e4df16a..6b53dfe 100644
--- a/room/room-compiler/src/main/kotlin/androidx/room/writer/DaoWriter.kt
+++ b/room/room-compiler/src/main/kotlin/androidx/room/writer/DaoWriter.kt
@@ -587,6 +587,28 @@
     }
 
     private fun createQueryMethodBody(method: ReadQueryMethod): XCodeBlock {
+        if (!method.queryResultBinder.isMigratedToDriver()) {
+            return compatCreateQueryMethodBody(method)
+        }
+
+        val scope = CodeGenScope(this, useDriverApi = true)
+        val queryWriter = QueryWriter(method)
+        val sqlVar = scope.getTmpVar("_sql")
+        val listSizeArgs = queryWriter.prepareQuery(sqlVar, scope)
+        method.queryResultBinder.convertAndReturn(
+            sqlQueryVar = sqlVar,
+            dbProperty = dbProperty,
+            bindStatement = { stmtVar ->
+                queryWriter.bindArgs(stmtVar, listSizeArgs, this)
+            },
+            returnTypeName = method.returnType.asTypeName(),
+            inTransaction = method.inTransaction,
+            scope = scope
+        )
+        return scope.generate()
+    }
+
+    private fun compatCreateQueryMethodBody(method: ReadQueryMethod): XCodeBlock {
         val queryWriter = QueryWriter(method)
         val scope = CodeGenScope(this)
         val sqlVar = scope.getTmpVar("_sql")
diff --git a/room/room-compiler/src/main/kotlin/androidx/room/writer/DatabaseWriter.kt b/room/room-compiler/src/main/kotlin/androidx/room/writer/DatabaseWriter.kt
index cd4ceca..82bd6f6 100644
--- a/room/room-compiler/src/main/kotlin/androidx/room/writer/DatabaseWriter.kt
+++ b/room/room-compiler/src/main/kotlin/androidx/room/writer/DatabaseWriter.kt
@@ -21,17 +21,16 @@
 import androidx.room.compiler.codegen.XCodeBlock
 import androidx.room.compiler.codegen.XCodeBlock.Builder.Companion.addLocalVal
 import androidx.room.compiler.codegen.XFunSpec
+import androidx.room.compiler.codegen.XFunSpec.Builder.Companion.addStatement
 import androidx.room.compiler.codegen.XPropertySpec
 import androidx.room.compiler.codegen.XPropertySpec.Companion.apply
 import androidx.room.compiler.codegen.XTypeName
 import androidx.room.compiler.codegen.XTypeSpec
 import androidx.room.compiler.codegen.XTypeSpec.Builder.Companion.addOriginatingElement
-import androidx.room.ext.AndroidTypeNames
 import androidx.room.ext.CommonTypeNames
 import androidx.room.ext.KotlinCollectionMemberNames
 import androidx.room.ext.KotlinTypeNames
 import androidx.room.ext.RoomTypeNames
-import androidx.room.ext.SupportDbTypeNames
 import androidx.room.ext.decapitalize
 import androidx.room.ext.stripNonJava
 import androidx.room.solver.CodeGenScope
@@ -60,7 +59,9 @@
             )
             addFunction(createOpenDelegate())
             addFunction(createCreateInvalidationTracker())
-            addFunction(createClearAllTables())
+            if (database.overrideClearAllTables) {
+                addFunction(createClearAllTables())
+            }
             addFunction(createCreateTypeConvertersMap())
             addFunction(createCreateAutoMigrationSpecsSet())
             addFunction(createGetAutoMigrations())
@@ -196,70 +197,19 @@
     }
 
     private fun createClearAllTables(): XFunSpec {
-        val scope = CodeGenScope(this)
-        val body = XCodeBlock.builder(codeLanguage).apply {
-            addStatement("super.assertNotMainThread()")
-            val dbVar = scope.getTmpVar("_db")
-            addLocalVal(
-                dbVar,
-                SupportDbTypeNames.DB,
-                when (language) {
-                    CodeLanguage.JAVA -> "super.getOpenHelper().getWritableDatabase()"
-                    CodeLanguage.KOTLIN -> "super.openHelper.writableDatabase"
-                }
-            )
-            val deferVar = scope.getTmpVar("_supportsDeferForeignKeys")
-            if (database.enableForeignKeys) {
-                addLocalVal(
-                    deferVar,
-                    XTypeName.PRIMITIVE_BOOLEAN,
-                    "%L.VERSION.SDK_INT >= %L.VERSION_CODES.LOLLIPOP",
-                    AndroidTypeNames.BUILD,
-                    AndroidTypeNames.BUILD
-                )
-            }
-            beginControlFlow("try").apply {
-                if (database.enableForeignKeys) {
-                    beginControlFlow("if (!%L)", deferVar).apply {
-                        addStatement("%L.execSQL(%S)", dbVar, "PRAGMA foreign_keys = FALSE")
-                    }
-                    endControlFlow()
-                }
-                addStatement("super.beginTransaction()")
-                if (database.enableForeignKeys) {
-                    beginControlFlow("if (%L)", deferVar).apply {
-                        addStatement("%L.execSQL(%S)", dbVar, "PRAGMA defer_foreign_keys = TRUE")
-                    }
-                    endControlFlow()
-                }
-                database.entities.sortedWith(EntityDeleteComparator()).forEach {
-                    addStatement("%L.execSQL(%S)", dbVar, "DELETE FROM `${it.tableName}`")
-                }
-                addStatement("super.setTransactionSuccessful()")
-            }
-            nextControlFlow("finally").apply {
-                addStatement("super.endTransaction()")
-                if (database.enableForeignKeys) {
-                    beginControlFlow("if (!%L)", deferVar).apply {
-                        addStatement("%L.execSQL(%S)", dbVar, "PRAGMA foreign_keys = TRUE")
-                    }
-                    endControlFlow()
-                }
-                addStatement("%L.query(%S).close()", dbVar, "PRAGMA wal_checkpoint(FULL)")
-                beginControlFlow("if (!%L.inTransaction())", dbVar).apply {
-                    addStatement("%L.execSQL(%S)", dbVar, "VACUUM")
-                }
-                endControlFlow()
-            }
-            endControlFlow()
-        }.build()
         return XFunSpec.builder(
             language = codeLanguage,
             name = "clearAllTables",
             visibility = VisibilityModifier.PUBLIC,
             isOverride = true
         ).apply {
-            addCode(body)
+            val tableNames = database.entities.sortedWith(EntityDeleteComparator())
+                .joinToString(", ") { "\"${it.tableName}\"" }
+            addStatement(
+                "super.performClear(%L, %L)",
+                database.enableForeignKeys,
+                tableNames
+            )
         }.build()
     }
 
@@ -267,12 +217,11 @@
         val scope = CodeGenScope(this)
         val body = XCodeBlock.builder(codeLanguage).apply {
             val shadowTablesVar = "_shadowTablesMap"
-            val shadowTablesTypeName = CommonTypeNames.HASH_MAP.parametrizedBy(
+            val shadowTablesTypeParam = arrayOf(
                 CommonTypeNames.STRING, CommonTypeNames.STRING
             )
-            val tableNames = database.entities.joinToString(",") {
-                "\"${it.tableName}\""
-            }
+            val shadowTablesTypeName =
+                CommonTypeNames.MUTABLE_MAP.parametrizedBy(*shadowTablesTypeParam)
             val shadowTableNames = database.entities.filter {
                 it.shadowTableName != null
             }.map {
@@ -281,41 +230,68 @@
             addLocalVariable(
                 name = shadowTablesVar,
                 typeName = shadowTablesTypeName,
-                assignExpr = XCodeBlock.ofNewInstance(
-                    codeLanguage,
-                    shadowTablesTypeName,
-                    "%L",
-                    shadowTableNames.size
-                )
+                assignExpr = when (language) {
+                    CodeLanguage.JAVA -> XCodeBlock.ofNewInstance(
+                        codeLanguage,
+                        CommonTypeNames.HASH_MAP.parametrizedBy(*shadowTablesTypeParam),
+                        "%L",
+                        shadowTableNames.size
+                    )
+
+                    CodeLanguage.KOTLIN -> XCodeBlock.of(
+                        language,
+                        "%M()",
+                        KotlinCollectionMemberNames.MUTABLE_MAP_OF
+                    )
+                }
             )
             shadowTableNames.forEach { (tableName, shadowTableName) ->
                 addStatement("%L.put(%S, %S)", shadowTablesVar, tableName, shadowTableName)
             }
             val viewTablesVar = scope.getTmpVar("_viewTables")
-            val tablesType = CommonTypeNames.HASH_SET.parametrizedBy(CommonTypeNames.STRING)
-            val viewTablesType = CommonTypeNames.HASH_MAP.parametrizedBy(
+            val viewTableTypeParam = arrayOf(
                 CommonTypeNames.STRING,
                 CommonTypeNames.SET.parametrizedBy(CommonTypeNames.STRING)
             )
+            val viewTablesTypeName = CommonTypeNames.MUTABLE_MAP.parametrizedBy(*viewTableTypeParam)
             addLocalVariable(
                 name = viewTablesVar,
-                typeName = viewTablesType,
-                assignExpr = XCodeBlock.ofNewInstance(
-                    codeLanguage,
-                    viewTablesType,
-                    "%L", database.views.size
-                )
+                typeName = viewTablesTypeName,
+                assignExpr = when (language) {
+                    CodeLanguage.JAVA -> XCodeBlock.ofNewInstance(
+                        codeLanguage,
+                        CommonTypeNames.HASH_MAP.parametrizedBy(*viewTableTypeParam),
+                        "%L",
+                        database.views.size
+                    )
+
+                    CodeLanguage.KOTLIN -> XCodeBlock.of(
+                        language,
+                        "%M()",
+                        KotlinCollectionMemberNames.MUTABLE_MAP_OF
+                    )
+                }
             )
+            val tablesType = CommonTypeNames.MUTABLE_SET.parametrizedBy(CommonTypeNames.STRING)
             for (view in database.views) {
                 val tablesVar = scope.getTmpVar("_tables")
                 addLocalVariable(
                     name = tablesVar,
                     typeName = tablesType,
-                    assignExpr = XCodeBlock.ofNewInstance(
-                        codeLanguage,
-                        tablesType,
-                        "%L", view.tables.size
-                    )
+                    assignExpr = when (language) {
+                        CodeLanguage.JAVA -> XCodeBlock.ofNewInstance(
+                            codeLanguage,
+                            CommonTypeNames.HASH_SET.parametrizedBy(CommonTypeNames.STRING),
+                            "%L",
+                            view.tables.size
+                        )
+
+                        CodeLanguage.KOTLIN -> XCodeBlock.of(
+                            language,
+                            "%M()",
+                            KotlinCollectionMemberNames.MUTABLE_SET_OF
+                        )
+                    }
                 )
                 for (table in view.tables) {
                     addStatement("%L.add(%S)", tablesVar, table)
@@ -325,6 +301,8 @@
                     viewTablesVar, view.viewName.lowercase(Locale.US), tablesVar
                 )
             }
+            val tableNames =
+                database.entities.joinToString(", ") { "\"${it.tableName}\"" }
             addStatement(
                 "return %L",
                 XCodeBlock.ofNewInstance(
diff --git a/room/room-compiler/src/main/kotlin/androidx/room/writer/FtsTableInfoValidationWriter.kt b/room/room-compiler/src/main/kotlin/androidx/room/writer/FtsTableInfoValidationWriter.kt
index d55ee3b..2e6106a 100644
--- a/room/room-compiler/src/main/kotlin/androidx/room/writer/FtsTableInfoValidationWriter.kt
+++ b/room/room-compiler/src/main/kotlin/androidx/room/writer/FtsTableInfoValidationWriter.kt
@@ -16,9 +16,11 @@
 
 package androidx.room.writer
 
+import androidx.room.compiler.codegen.CodeLanguage
 import androidx.room.compiler.codegen.XCodeBlock
 import androidx.room.compiler.codegen.XCodeBlock.Builder.Companion.addLocalVal
 import androidx.room.ext.CommonTypeNames
+import androidx.room.ext.KotlinCollectionMemberNames
 import androidx.room.ext.RoomMemberNames
 import androidx.room.ext.RoomTypeNames
 import androidx.room.ext.capitalize
@@ -32,16 +34,23 @@
         val expectedInfoVar = scope.getTmpVar("_info$suffix")
         scope.builder.apply {
             val columnSetVar = scope.getTmpVar("_columns$suffix")
-            val columnsSetType = CommonTypeNames.HASH_SET.parametrizedBy(CommonTypeNames.STRING)
+            val columnsSetType = CommonTypeNames.MUTABLE_SET.parametrizedBy(CommonTypeNames.STRING)
             addLocalVariable(
                 name = columnSetVar,
                 typeName = columnsSetType,
-                assignExpr = XCodeBlock.ofNewInstance(
-                    language,
-                    columnsSetType,
-                    "%L",
-                    entity.fields.size
-                )
+                assignExpr = when (language) {
+                    CodeLanguage.JAVA -> XCodeBlock.ofNewInstance(
+                        language,
+                        CommonTypeNames.HASH_SET.parametrizedBy(CommonTypeNames.STRING),
+                        "%L",
+                        entity.fields.size
+                    )
+                    CodeLanguage.KOTLIN -> XCodeBlock.of(
+                        language,
+                        "%M()",
+                        KotlinCollectionMemberNames.MUTABLE_SET_OF
+                    )
+                }
             )
             entity.nonHiddenFields.forEach {
                 addStatement("%L.add(%S)", columnSetVar, it.columnName)
diff --git a/room/room-compiler/src/main/kotlin/androidx/room/writer/TableInfoValidationWriter.kt b/room/room-compiler/src/main/kotlin/androidx/room/writer/TableInfoValidationWriter.kt
index 8646d11..b633e460 100644
--- a/room/room-compiler/src/main/kotlin/androidx/room/writer/TableInfoValidationWriter.kt
+++ b/room/room-compiler/src/main/kotlin/androidx/room/writer/TableInfoValidationWriter.kt
@@ -20,6 +20,7 @@
 import androidx.room.compiler.codegen.XCodeBlock
 import androidx.room.compiler.codegen.XCodeBlock.Builder.Companion.addLocalVal
 import androidx.room.ext.CommonTypeNames
+import androidx.room.ext.KotlinCollectionMemberNames
 import androidx.room.ext.RoomMemberNames
 import androidx.room.ext.RoomTypeNames
 import androidx.room.ext.capitalize
@@ -40,19 +41,29 @@
         val expectedInfoVar = scope.getTmpVar("_info$suffix")
         scope.builder.apply {
             val columnListVar = scope.getTmpVar("_columns$suffix")
-            val columnListType = CommonTypeNames.HASH_MAP.parametrizedBy(
+            val columnListType = CommonTypeNames.MUTABLE_MAP.parametrizedBy(
                 CommonTypeNames.STRING,
                 RoomTypeNames.TABLE_INFO_COLUMN
             )
             addLocalVariable(
                 name = columnListVar,
                 typeName = columnListType,
-                assignExpr = XCodeBlock.ofNewInstance(
-                    language,
-                    columnListType,
-                    "%L",
-                    entity.fields.size
-                )
+                assignExpr = when (language) {
+                    CodeLanguage.JAVA -> XCodeBlock.ofNewInstance(
+                        language,
+                        CommonTypeNames.HASH_MAP.parametrizedBy(
+                            CommonTypeNames.STRING,
+                            RoomTypeNames.TABLE_INFO_COLUMN
+                        ),
+                        "%L",
+                        entity.fields.size
+                    )
+                    CodeLanguage.KOTLIN -> XCodeBlock.of(
+                        language,
+                        "%M()",
+                        KotlinCollectionMemberNames.MUTABLE_MAP_OF
+                    )
+                }
             )
             entity.fields.forEach { field ->
                 addStatement(
@@ -75,16 +86,25 @@
 
             val foreignKeySetVar = scope.getTmpVar("_foreignKeys$suffix")
             val foreignKeySetType =
-                CommonTypeNames.HASH_SET.parametrizedBy(RoomTypeNames.TABLE_INFO_FOREIGN_KEY)
+                CommonTypeNames.MUTABLE_SET.parametrizedBy(RoomTypeNames.TABLE_INFO_FOREIGN_KEY)
             addLocalVariable(
                 name = foreignKeySetVar,
                 typeName = foreignKeySetType,
-                assignExpr = XCodeBlock.ofNewInstance(
-                    language,
-                    foreignKeySetType,
-                    "%L",
-                    entity.foreignKeys.size
-                )
+                assignExpr = when (language) {
+                    CodeLanguage.JAVA -> XCodeBlock.ofNewInstance(
+                        language,
+                        CommonTypeNames.HASH_SET.parametrizedBy(
+                            RoomTypeNames.TABLE_INFO_FOREIGN_KEY
+                        ),
+                        "%L",
+                        entity.foreignKeys.size
+                    )
+                    CodeLanguage.KOTLIN -> XCodeBlock.of(
+                        language,
+                        "%M()",
+                        KotlinCollectionMemberNames.MUTABLE_SET_OF
+                    )
+                }
             )
             entity.foreignKeys.forEach {
                 addStatement(
@@ -105,16 +125,25 @@
 
             val indicesSetVar = scope.getTmpVar("_indices$suffix")
             val indicesType =
-                CommonTypeNames.HASH_SET.parametrizedBy(RoomTypeNames.TABLE_INFO_INDEX)
+                CommonTypeNames.MUTABLE_SET.parametrizedBy(RoomTypeNames.TABLE_INFO_INDEX)
             addLocalVariable(
                 name = indicesSetVar,
                 typeName = indicesType,
-                assignExpr = XCodeBlock.ofNewInstance(
-                    language,
-                    indicesType,
-                    "%L",
-                    entity.indices.size
-                )
+                assignExpr = when (language) {
+                    CodeLanguage.JAVA -> XCodeBlock.ofNewInstance(
+                        language,
+                        CommonTypeNames.HASH_SET.parametrizedBy(
+                            RoomTypeNames.TABLE_INFO_INDEX
+                        ),
+                        "%L",
+                        entity.indices.size
+                    )
+                    CodeLanguage.KOTLIN -> XCodeBlock.of(
+                        language,
+                        "%M()",
+                        KotlinCollectionMemberNames.MUTABLE_SET_OF
+                    )
+                }
             )
             entity.indices.forEach { index ->
                 val orders = if (index.orders.isEmpty()) {
diff --git a/room/room-compiler/src/test/kotlin/androidx/room/verifier/DatabaseVerifierTest.kt b/room/room-compiler/src/test/kotlin/androidx/room/verifier/DatabaseVerifierTest.kt
index 0abf406..2c37b7d 100644
--- a/room/room-compiler/src/test/kotlin/androidx/room/verifier/DatabaseVerifierTest.kt
+++ b/room/room-compiler/src/test/kotlin/androidx/room/verifier/DatabaseVerifierTest.kt
@@ -376,7 +376,8 @@
             daoMethods = emptyList(),
             version = -1,
             exportSchema = false,
-            enableForeignKeys = false
+            enableForeignKeys = false,
+            overrideClearAllTables = true,
         )
     }
 
diff --git a/room/room-compiler/src/test/kotlin/androidx/room/vo/DatabaseTest.kt b/room/room-compiler/src/test/kotlin/androidx/room/vo/DatabaseTest.kt
index 6370004..aa1b9b7 100644
--- a/room/room-compiler/src/test/kotlin/androidx/room/vo/DatabaseTest.kt
+++ b/room/room-compiler/src/test/kotlin/androidx/room/vo/DatabaseTest.kt
@@ -66,7 +66,8 @@
             daoMethods = emptyList(),
             version = 1,
             exportSchema = false,
-            enableForeignKeys = false
+            enableForeignKeys = false,
+            overrideClearAllTables = true
         )
 
         val expectedLegacyHash = DigestUtils.md5Hex(
diff --git a/room/room-compiler/src/test/test-data/daoWriter/output/javac/ComplexDao.java b/room/room-compiler/src/test/test-data/daoWriter/output/javac/ComplexDao.java
index 1d1f41f..6edd091 100644
--- a/room/room-compiler/src/test/test-data/daoWriter/output/javac/ComplexDao.java
+++ b/room/room-compiler/src/test/test-data/daoWriter/output/javac/ComplexDao.java
@@ -10,7 +10,9 @@
 import androidx.room.guava.GuavaRoom;
 import androidx.room.util.CursorUtil;
 import androidx.room.util.DBUtil;
+import androidx.room.util.SQLiteStatementUtil;
 import androidx.room.util.StringUtil;
+import androidx.sqlite.SQLiteStatement;
 import androidx.sqlite.db.SupportSQLiteQuery;
 import com.google.common.util.concurrent.ListenableFuture;
 import java.lang.Class;
@@ -25,6 +27,7 @@
 import java.util.List;
 import java.util.concurrent.Callable;
 import javax.annotation.processing.Generated;
+import kotlin.jvm.functions.Function1;
 
 @Generated("androidx.room.RoomProcessor")
 @SuppressWarnings({"unchecked", "deprecation"})
@@ -82,91 +85,87 @@
     @Override
     public User getById(final int id) {
         final String _sql = "SELECT * FROM user where uid = ?";
-        final RoomSQLiteQuery _statement = RoomSQLiteQuery.acquire(_sql, 1);
-        int _argIndex = 1;
-        _statement.bindLong(_argIndex, id);
-        __db.assertNotSuspendingTransaction();
-        final Cursor _cursor = DBUtil.query(__db, _statement, false, null);
-        try {
-            final int _cursorIndexOfUid = CursorUtil.getColumnIndexOrThrow(_cursor, "uid");
-            final int _cursorIndexOfName = CursorUtil.getColumnIndexOrThrow(_cursor, "name");
-            final int _cursorIndexOfLastName = CursorUtil.getColumnIndexOrThrow(_cursor, "lastName");
-            final int _cursorIndexOfAge = CursorUtil.getColumnIndexOrThrow(_cursor, "ageColumn");
-            final User _result;
-            if (_cursor.moveToFirst()) {
-                _result = new User();
-                _result.uid = _cursor.getInt(_cursorIndexOfUid);
-                if (_cursor.isNull(_cursorIndexOfName)) {
-                    _result.name = null;
+        return DBUtil.performReadBlocking(__db, _sql, new Function1<SQLiteStatement, User>() {
+            @Override
+            @NonNull
+            public User invoke(@NonNull final SQLiteStatement _stmt) {
+                int _argIndex = 1;
+                _stmt.bindLong(_argIndex, id);
+                final int _cursorIndexOfUid = SQLiteStatementUtil.getColumnIndexOrThrow(_stmt, "uid");
+                final int _cursorIndexOfName = SQLiteStatementUtil.getColumnIndexOrThrow(_stmt, "name");
+                final int _cursorIndexOfLastName = SQLiteStatementUtil.getColumnIndexOrThrow(_stmt, "lastName");
+                final int _cursorIndexOfAge = SQLiteStatementUtil.getColumnIndexOrThrow(_stmt, "ageColumn");
+                final User _result;
+                if (_stmt.step()) {
+                    _result = new User();
+                    _result.uid = (int) (_stmt.getLong(_cursorIndexOfUid));
+                    if (_stmt.isNull(_cursorIndexOfName)) {
+                        _result.name = null;
+                    } else {
+                        _result.name = _stmt.getText(_cursorIndexOfName);
+                    }
+                    final String _tmpLastName;
+                    if (_stmt.isNull(_cursorIndexOfLastName)) {
+                        _tmpLastName = null;
+                    } else {
+                        _tmpLastName = _stmt.getText(_cursorIndexOfLastName);
+                    }
+                    _result.setLastName(_tmpLastName);
+                    _result.age = (int) (_stmt.getLong(_cursorIndexOfAge));
                 } else {
-                    _result.name = _cursor.getString(_cursorIndexOfName);
+                    _result = null;
                 }
-                final String _tmpLastName;
-                if (_cursor.isNull(_cursorIndexOfLastName)) {
-                    _tmpLastName = null;
-                } else {
-                    _tmpLastName = _cursor.getString(_cursorIndexOfLastName);
-                }
-                _result.setLastName(_tmpLastName);
-                _result.age = _cursor.getInt(_cursorIndexOfAge);
-            } else {
-                _result = null;
+                return _result;
             }
-            return _result;
-        } finally {
-            _cursor.close();
-            _statement.release();
-        }
+        });
     }
 
     @Override
     public User findByName(final String name, final String lastName) {
         final String _sql = "SELECT * FROM user where name LIKE ? AND lastName LIKE ?";
-        final RoomSQLiteQuery _statement = RoomSQLiteQuery.acquire(_sql, 2);
-        int _argIndex = 1;
-        if (name == null) {
-            _statement.bindNull(_argIndex);
-        } else {
-            _statement.bindString(_argIndex, name);
-        }
-        _argIndex = 2;
-        if (lastName == null) {
-            _statement.bindNull(_argIndex);
-        } else {
-            _statement.bindString(_argIndex, lastName);
-        }
-        __db.assertNotSuspendingTransaction();
-        final Cursor _cursor = DBUtil.query(__db, _statement, false, null);
-        try {
-            final int _cursorIndexOfUid = CursorUtil.getColumnIndexOrThrow(_cursor, "uid");
-            final int _cursorIndexOfName = CursorUtil.getColumnIndexOrThrow(_cursor, "name");
-            final int _cursorIndexOfLastName = CursorUtil.getColumnIndexOrThrow(_cursor, "lastName");
-            final int _cursorIndexOfAge = CursorUtil.getColumnIndexOrThrow(_cursor, "ageColumn");
-            final User _result;
-            if (_cursor.moveToFirst()) {
-                _result = new User();
-                _result.uid = _cursor.getInt(_cursorIndexOfUid);
-                if (_cursor.isNull(_cursorIndexOfName)) {
-                    _result.name = null;
+        return DBUtil.performReadBlocking(__db, _sql, new Function1<SQLiteStatement, User>() {
+            @Override
+            @NonNull
+            public User invoke(@NonNull final SQLiteStatement _stmt) {
+                int _argIndex = 1;
+                if (name == null) {
+                    _stmt.bindNull(_argIndex);
                 } else {
-                    _result.name = _cursor.getString(_cursorIndexOfName);
+                    _stmt.bindText(_argIndex, name);
                 }
-                final String _tmpLastName;
-                if (_cursor.isNull(_cursorIndexOfLastName)) {
-                    _tmpLastName = null;
+                _argIndex = 2;
+                if (lastName == null) {
+                    _stmt.bindNull(_argIndex);
                 } else {
-                    _tmpLastName = _cursor.getString(_cursorIndexOfLastName);
+                    _stmt.bindText(_argIndex, lastName);
                 }
-                _result.setLastName(_tmpLastName);
-                _result.age = _cursor.getInt(_cursorIndexOfAge);
-            } else {
-                _result = null;
+                final int _cursorIndexOfUid = SQLiteStatementUtil.getColumnIndexOrThrow(_stmt, "uid");
+                final int _cursorIndexOfName = SQLiteStatementUtil.getColumnIndexOrThrow(_stmt, "name");
+                final int _cursorIndexOfLastName = SQLiteStatementUtil.getColumnIndexOrThrow(_stmt, "lastName");
+                final int _cursorIndexOfAge = SQLiteStatementUtil.getColumnIndexOrThrow(_stmt, "ageColumn");
+                final User _result;
+                if (_stmt.step()) {
+                    _result = new User();
+                    _result.uid = (int) (_stmt.getLong(_cursorIndexOfUid));
+                    if (_stmt.isNull(_cursorIndexOfName)) {
+                        _result.name = null;
+                    } else {
+                        _result.name = _stmt.getText(_cursorIndexOfName);
+                    }
+                    final String _tmpLastName;
+                    if (_stmt.isNull(_cursorIndexOfLastName)) {
+                        _tmpLastName = null;
+                    } else {
+                        _tmpLastName = _stmt.getText(_cursorIndexOfLastName);
+                    }
+                    _result.setLastName(_tmpLastName);
+                    _result.age = (int) (_stmt.getLong(_cursorIndexOfAge));
+                } else {
+                    _result = null;
+                }
+                return _result;
             }
-            return _result;
-        } finally {
-            _cursor.close();
-            _statement.release();
-        }
+        });
     }
 
     @Override
diff --git a/room/room-compiler/src/test/test-data/daoWriter/output/ksp/ComplexDao.java b/room/room-compiler/src/test/test-data/daoWriter/output/ksp/ComplexDao.java
index 3688bea..5241793 100644
--- a/room/room-compiler/src/test/test-data/daoWriter/output/ksp/ComplexDao.java
+++ b/room/room-compiler/src/test/test-data/daoWriter/output/ksp/ComplexDao.java
@@ -10,7 +10,9 @@
 import androidx.room.guava.GuavaRoom;
 import androidx.room.util.CursorUtil;
 import androidx.room.util.DBUtil;
+import androidx.room.util.SQLiteStatementUtil;
 import androidx.room.util.StringUtil;
+import androidx.sqlite.SQLiteStatement;
 import androidx.sqlite.db.SupportSQLiteQuery;
 import com.google.common.util.concurrent.ListenableFuture;
 import java.lang.Class;
@@ -25,6 +27,7 @@
 import java.util.List;
 import java.util.concurrent.Callable;
 import javax.annotation.processing.Generated;
+import kotlin.jvm.functions.Function1;
 
 @Generated("androidx.room.RoomProcessor")
 @SuppressWarnings({"unchecked", "deprecation"})
@@ -78,67 +81,63 @@
     @Override
     public User getById(final int id) {
         final String _sql = "SELECT * FROM user where uid = ?";
-        final RoomSQLiteQuery _statement = RoomSQLiteQuery.acquire(_sql, 1);
-        int _argIndex = 1;
-        _statement.bindLong(_argIndex, id);
-        __db.assertNotSuspendingTransaction();
-        final Cursor _cursor = DBUtil.query(__db, _statement, false, null);
-        try {
-            final int _cursorIndexOfUid = CursorUtil.getColumnIndexOrThrow(_cursor, "uid");
-            final int _cursorIndexOfName = CursorUtil.getColumnIndexOrThrow(_cursor, "name");
-            final int _cursorIndexOfLastName = CursorUtil.getColumnIndexOrThrow(_cursor, "lastName");
-            final int _cursorIndexOfAge = CursorUtil.getColumnIndexOrThrow(_cursor, "ageColumn");
-            final User _result;
-            if (_cursor.moveToFirst()) {
-                _result = new User();
-                _result.uid = _cursor.getInt(_cursorIndexOfUid);
-                _result.name = _cursor.getString(_cursorIndexOfName);
-                final String _tmpLastName;
-                _tmpLastName = _cursor.getString(_cursorIndexOfLastName);
-                _result.setLastName(_tmpLastName);
-                _result.age = _cursor.getInt(_cursorIndexOfAge);
-            } else {
-                _result = null;
+        return DBUtil.performReadBlocking(__db, _sql, new Function1<SQLiteStatement, User>() {
+            @Override
+            @NonNull
+            public User invoke(@NonNull final SQLiteStatement _stmt) {
+                int _argIndex = 1;
+                _stmt.bindLong(_argIndex, id);
+                final int _cursorIndexOfUid = SQLiteStatementUtil.getColumnIndexOrThrow(_stmt, "uid");
+                final int _cursorIndexOfName = SQLiteStatementUtil.getColumnIndexOrThrow(_stmt, "name");
+                final int _cursorIndexOfLastName = SQLiteStatementUtil.getColumnIndexOrThrow(_stmt, "lastName");
+                final int _cursorIndexOfAge = SQLiteStatementUtil.getColumnIndexOrThrow(_stmt, "ageColumn");
+                final User _result;
+                if (_stmt.step()) {
+                    _result = new User();
+                    _result.uid = (int) (_stmt.getLong(_cursorIndexOfUid));
+                    _result.name = _stmt.getText(_cursorIndexOfName);
+                    final String _tmpLastName;
+                    _tmpLastName = _stmt.getText(_cursorIndexOfLastName);
+                    _result.setLastName(_tmpLastName);
+                    _result.age = (int) (_stmt.getLong(_cursorIndexOfAge));
+                } else {
+                    _result = null;
+                }
+                return _result;
             }
-            return _result;
-        } finally {
-            _cursor.close();
-            _statement.release();
-        }
+        });
     }
 
     @Override
     public User findByName(final String name, final String lastName) {
         final String _sql = "SELECT * FROM user where name LIKE ? AND lastName LIKE ?";
-        final RoomSQLiteQuery _statement = RoomSQLiteQuery.acquire(_sql, 2);
-        int _argIndex = 1;
-        _statement.bindString(_argIndex, name);
-        _argIndex = 2;
-        _statement.bindString(_argIndex, lastName);
-        __db.assertNotSuspendingTransaction();
-        final Cursor _cursor = DBUtil.query(__db, _statement, false, null);
-        try {
-            final int _cursorIndexOfUid = CursorUtil.getColumnIndexOrThrow(_cursor, "uid");
-            final int _cursorIndexOfName = CursorUtil.getColumnIndexOrThrow(_cursor, "name");
-            final int _cursorIndexOfLastName = CursorUtil.getColumnIndexOrThrow(_cursor, "lastName");
-            final int _cursorIndexOfAge = CursorUtil.getColumnIndexOrThrow(_cursor, "ageColumn");
-            final User _result;
-            if (_cursor.moveToFirst()) {
-                _result = new User();
-                _result.uid = _cursor.getInt(_cursorIndexOfUid);
-                _result.name = _cursor.getString(_cursorIndexOfName);
-                final String _tmpLastName;
-                _tmpLastName = _cursor.getString(_cursorIndexOfLastName);
-                _result.setLastName(_tmpLastName);
-                _result.age = _cursor.getInt(_cursorIndexOfAge);
-            } else {
-                _result = null;
+        return DBUtil.performReadBlocking(__db, _sql, new Function1<SQLiteStatement, User>() {
+            @Override
+            @NonNull
+            public User invoke(@NonNull final SQLiteStatement _stmt) {
+                int _argIndex = 1;
+                _stmt.bindText(_argIndex, name);
+                _argIndex = 2;
+                _stmt.bindText(_argIndex, lastName);
+                final int _cursorIndexOfUid = SQLiteStatementUtil.getColumnIndexOrThrow(_stmt, "uid");
+                final int _cursorIndexOfName = SQLiteStatementUtil.getColumnIndexOrThrow(_stmt, "name");
+                final int _cursorIndexOfLastName = SQLiteStatementUtil.getColumnIndexOrThrow(_stmt, "lastName");
+                final int _cursorIndexOfAge = SQLiteStatementUtil.getColumnIndexOrThrow(_stmt, "ageColumn");
+                final User _result;
+                if (_stmt.step()) {
+                    _result = new User();
+                    _result.uid = (int) (_stmt.getLong(_cursorIndexOfUid));
+                    _result.name = _stmt.getText(_cursorIndexOfName);
+                    final String _tmpLastName;
+                    _tmpLastName = _stmt.getText(_cursorIndexOfLastName);
+                    _result.setLastName(_tmpLastName);
+                    _result.age = (int) (_stmt.getLong(_cursorIndexOfAge));
+                } else {
+                    _result = null;
+                }
+                return _result;
             }
-            return _result;
-        } finally {
-            _cursor.close();
-            _statement.release();
-        }
+        });
     }
 
     @Override
diff --git a/room/room-compiler/src/test/test-data/databasewriter/output/ComplexDatabase.java b/room/room-compiler/src/test/test-data/databasewriter/output/ComplexDatabase.java
index 36110cc..f4a1802 100644
--- a/room/room-compiler/src/test/test-data/databasewriter/output/ComplexDatabase.java
+++ b/room/room-compiler/src/test/test-data/databasewriter/output/ComplexDatabase.java
@@ -10,7 +10,6 @@
 import androidx.room.util.ViewInfo;
 import androidx.sqlite.SQLiteConnection;
 import androidx.sqlite.SQLiteKt;
-import androidx.sqlite.db.SupportSQLiteDatabase;
 import java.lang.Class;
 import java.lang.Override;
 import java.lang.String;
@@ -72,13 +71,13 @@
             @NonNull
             public RoomOpenDelegate.ValidationResult onValidateSchema(
                     @NonNull final SQLiteConnection connection) {
-                final HashMap<String, TableInfo.Column> _columnsUser = new HashMap<String, TableInfo.Column>(4);
+                final Map<String, TableInfo.Column> _columnsUser = new HashMap<String, TableInfo.Column>(4);
                 _columnsUser.put("uid", new TableInfo.Column("uid", "INTEGER", true, 1, null, TableInfo.CREATED_FROM_ENTITY));
                 _columnsUser.put("name", new TableInfo.Column("name", "TEXT", false, 0, null, TableInfo.CREATED_FROM_ENTITY));
                 _columnsUser.put("lastName", new TableInfo.Column("lastName", "TEXT", false, 0, null, TableInfo.CREATED_FROM_ENTITY));
                 _columnsUser.put("ageColumn", new TableInfo.Column("ageColumn", "INTEGER", true, 0, null, TableInfo.CREATED_FROM_ENTITY));
-                final HashSet<TableInfo.ForeignKey> _foreignKeysUser = new HashSet<TableInfo.ForeignKey>(0);
-                final HashSet<TableInfo.Index> _indicesUser = new HashSet<TableInfo.Index>(0);
+                final Set<TableInfo.ForeignKey> _foreignKeysUser = new HashSet<TableInfo.ForeignKey>(0);
+                final Set<TableInfo.Index> _indicesUser = new HashSet<TableInfo.Index>(0);
                 final TableInfo _infoUser = new TableInfo("User", _columnsUser, _foreignKeysUser, _indicesUser);
                 final TableInfo _existingUser = TableInfo.read(connection, "User");
                 if (!_infoUser.equals(_existingUser)) {
@@ -86,13 +85,13 @@
                             + " Expected:\n" + _infoUser + "\n"
                             + " Found:\n" + _existingUser);
                 }
-                final HashMap<String, TableInfo.Column> _columnsChild1 = new HashMap<String, TableInfo.Column>(4);
+                final Map<String, TableInfo.Column> _columnsChild1 = new HashMap<String, TableInfo.Column>(4);
                 _columnsChild1.put("id", new TableInfo.Column("id", "INTEGER", true, 1, null, TableInfo.CREATED_FROM_ENTITY));
                 _columnsChild1.put("name", new TableInfo.Column("name", "TEXT", false, 0, null, TableInfo.CREATED_FROM_ENTITY));
                 _columnsChild1.put("serial", new TableInfo.Column("serial", "INTEGER", false, 0, null, TableInfo.CREATED_FROM_ENTITY));
                 _columnsChild1.put("code", new TableInfo.Column("code", "TEXT", false, 0, null, TableInfo.CREATED_FROM_ENTITY));
-                final HashSet<TableInfo.ForeignKey> _foreignKeysChild1 = new HashSet<TableInfo.ForeignKey>(0);
-                final HashSet<TableInfo.Index> _indicesChild1 = new HashSet<TableInfo.Index>(0);
+                final Set<TableInfo.ForeignKey> _foreignKeysChild1 = new HashSet<TableInfo.ForeignKey>(0);
+                final Set<TableInfo.Index> _indicesChild1 = new HashSet<TableInfo.Index>(0);
                 final TableInfo _infoChild1 = new TableInfo("Child1", _columnsChild1, _foreignKeysChild1, _indicesChild1);
                 final TableInfo _existingChild1 = TableInfo.read(connection, "Child1");
                 if (!_infoChild1.equals(_existingChild1)) {
@@ -100,13 +99,13 @@
                             + " Expected:\n" + _infoChild1 + "\n"
                             + " Found:\n" + _existingChild1);
                 }
-                final HashMap<String, TableInfo.Column> _columnsChild2 = new HashMap<String, TableInfo.Column>(4);
+                final Map<String, TableInfo.Column> _columnsChild2 = new HashMap<String, TableInfo.Column>(4);
                 _columnsChild2.put("id", new TableInfo.Column("id", "INTEGER", true, 1, null, TableInfo.CREATED_FROM_ENTITY));
                 _columnsChild2.put("name", new TableInfo.Column("name", "TEXT", false, 0, null, TableInfo.CREATED_FROM_ENTITY));
                 _columnsChild2.put("serial", new TableInfo.Column("serial", "INTEGER", false, 0, null, TableInfo.CREATED_FROM_ENTITY));
                 _columnsChild2.put("code", new TableInfo.Column("code", "TEXT", false, 0, null, TableInfo.CREATED_FROM_ENTITY));
-                final HashSet<TableInfo.ForeignKey> _foreignKeysChild2 = new HashSet<TableInfo.ForeignKey>(0);
-                final HashSet<TableInfo.Index> _indicesChild2 = new HashSet<TableInfo.Index>(0);
+                final Set<TableInfo.ForeignKey> _foreignKeysChild2 = new HashSet<TableInfo.ForeignKey>(0);
+                final Set<TableInfo.Index> _indicesChild2 = new HashSet<TableInfo.Index>(0);
                 final TableInfo _infoChild2 = new TableInfo("Child2", _columnsChild2, _foreignKeysChild2, _indicesChild2);
                 final TableInfo _existingChild2 = TableInfo.read(connection, "Child2");
                 if (!_infoChild2.equals(_existingChild2)) {
@@ -130,31 +129,17 @@
     @Override
     @NonNull
     protected InvalidationTracker createInvalidationTracker() {
-        final HashMap<String, String> _shadowTablesMap = new HashMap<String, String>(0);
-        final HashMap<String, Set<String>> _viewTables = new HashMap<String, Set<String>>(1);
-        final HashSet<String> _tables = new HashSet<String>(1);
+        final Map<String, String> _shadowTablesMap = new HashMap<String, String>(0);
+        final Map<String, Set<String>> _viewTables = new HashMap<String, Set<String>>(1);
+        final Set<String> _tables = new HashSet<String>(1);
         _tables.add("User");
         _viewTables.put("usersummary", _tables);
-        return new InvalidationTracker(this, _shadowTablesMap, _viewTables, "User","Child1","Child2");
+        return new InvalidationTracker(this, _shadowTablesMap, _viewTables, "User", "Child1", "Child2");
     }
 
     @Override
     public void clearAllTables() {
-        super.assertNotMainThread();
-        final SupportSQLiteDatabase _db = super.getOpenHelper().getWritableDatabase();
-        try {
-            super.beginTransaction();
-            _db.execSQL("DELETE FROM `User`");
-            _db.execSQL("DELETE FROM `Child1`");
-            _db.execSQL("DELETE FROM `Child2`");
-            super.setTransactionSuccessful();
-        } finally {
-            super.endTransaction();
-            _db.query("PRAGMA wal_checkpoint(FULL)").close();
-            if (!_db.inTransaction()) {
-                _db.execSQL("VACUUM");
-            }
-        }
+        super.performClear(false, "User", "Child1", "Child2");
     }
 
     @Override
diff --git a/room/room-compiler/src/test/test-data/kotlinCodeGen/abstractClassWithParam.kt b/room/room-compiler/src/test/test-data/kotlinCodeGen/abstractClassWithParam.kt
index 2364ea1..dacc6d7 100644
--- a/room/room-compiler/src/test/test-data/kotlinCodeGen/abstractClassWithParam.kt
+++ b/room/room-compiler/src/test/test-data/kotlinCodeGen/abstractClassWithParam.kt
@@ -1,9 +1,6 @@
-import android.database.Cursor
 import androidx.room.RoomDatabase
-import androidx.room.RoomSQLiteQuery
-import androidx.room.RoomSQLiteQuery.Companion.acquire
 import androidx.room.util.getColumnIndexOrThrow
-import androidx.room.util.query
+import androidx.room.util.performReadBlocking
 import javax.`annotation`.processing.Generated
 import kotlin.Int
 import kotlin.String
@@ -24,23 +21,17 @@
 
   public override fun getEntity(): MyEntity {
     val _sql: String = "SELECT * FROM MyEntity"
-    val _statement: RoomSQLiteQuery = acquire(_sql, 0)
-    __db.assertNotSuspendingTransaction()
-    val _cursor: Cursor = query(__db, _statement, false, null)
-    try {
-      val _cursorIndexOfPk: Int = getColumnIndexOrThrow(_cursor, "pk")
+    return performReadBlocking(__db, _sql) { _stmt ->
+      val _cursorIndexOfPk: Int = getColumnIndexOrThrow(_stmt, "pk")
       val _result: MyEntity
-      if (_cursor.moveToFirst()) {
+      if (_stmt.step()) {
         val _tmpPk: Int
-        _tmpPk = _cursor.getInt(_cursorIndexOfPk)
+        _tmpPk = _stmt.getLong(_cursorIndexOfPk).toInt()
         _result = MyEntity(_tmpPk)
       } else {
         error("The query result was empty, but expected a single row to return a NON-NULL object of type <MyEntity>.")
       }
-      return _result
-    } finally {
-      _cursor.close()
-      _statement.release()
+      _result
     }
   }
 
diff --git a/room/room-compiler/src/test/test-data/kotlinCodeGen/arrayParameterAdapter.kt b/room/room-compiler/src/test/test-data/kotlinCodeGen/arrayParameterAdapter.kt
index 4b29902..40d8ff1 100644
--- a/room/room-compiler/src/test/test-data/kotlinCodeGen/arrayParameterAdapter.kt
+++ b/room/room-compiler/src/test/test-data/kotlinCodeGen/arrayParameterAdapter.kt
@@ -1,11 +1,8 @@
-import android.database.Cursor
 import androidx.room.RoomDatabase
-import androidx.room.RoomSQLiteQuery
-import androidx.room.RoomSQLiteQuery.Companion.acquire
 import androidx.room.util.appendPlaceholders
 import androidx.room.util.getColumnIndexOrThrow
 import androidx.room.util.newStringBuilder
-import androidx.room.util.query
+import androidx.room.util.performReadBlocking
 import java.lang.StringBuilder
 import javax.`annotation`.processing.Generated
 import kotlin.Array
@@ -34,29 +31,22 @@
     appendPlaceholders(_stringBuilder, _inputSize)
     _stringBuilder.append(")")
     val _sql: String = _stringBuilder.toString()
-    val _argCount: Int = 0 + _inputSize
-    val _statement: RoomSQLiteQuery = acquire(_sql, _argCount)
-    var _argIndex: Int = 1
-    for (_item: String in arg) {
-      _statement.bindString(_argIndex, _item)
-      _argIndex++
-    }
-    __db.assertNotSuspendingTransaction()
-    val _cursor: Cursor = query(__db, _statement, false, null)
-    try {
-      val _cursorIndexOfId: Int = getColumnIndexOrThrow(_cursor, "id")
+    return performReadBlocking(__db, _sql) { _stmt ->
+      var _argIndex: Int = 1
+      for (_item: String in arg) {
+        _stmt.bindText(_argIndex, _item)
+        _argIndex++
+      }
+      val _cursorIndexOfId: Int = getColumnIndexOrThrow(_stmt, "id")
       val _result: MyEntity
-      if (_cursor.moveToFirst()) {
+      if (_stmt.step()) {
         val _tmpId: String
-        _tmpId = _cursor.getString(_cursorIndexOfId)
+        _tmpId = _stmt.getText(_cursorIndexOfId)
         _result = MyEntity(_tmpId)
       } else {
         error("The query result was empty, but expected a single row to return a NON-NULL object of type <MyEntity>.")
       }
-      return _result
-    } finally {
-      _cursor.close()
-      _statement.release()
+      _result
     }
   }
 
@@ -67,33 +57,26 @@
     appendPlaceholders(_stringBuilder, _inputSize)
     _stringBuilder.append(")")
     val _sql: String = _stringBuilder.toString()
-    val _argCount: Int = 0 + _inputSize
-    val _statement: RoomSQLiteQuery = acquire(_sql, _argCount)
-    var _argIndex: Int = 1
-    if (arg == null) {
-      _statement.bindNull(_argIndex)
-    } else {
-      for (_item: String in arg) {
-        _statement.bindString(_argIndex, _item)
-        _argIndex++
+    return performReadBlocking(__db, _sql) { _stmt ->
+      var _argIndex: Int = 1
+      if (arg == null) {
+        _stmt.bindNull(_argIndex)
+      } else {
+        for (_item: String in arg) {
+          _stmt.bindText(_argIndex, _item)
+          _argIndex++
+        }
       }
-    }
-    __db.assertNotSuspendingTransaction()
-    val _cursor: Cursor = query(__db, _statement, false, null)
-    try {
-      val _cursorIndexOfId: Int = getColumnIndexOrThrow(_cursor, "id")
+      val _cursorIndexOfId: Int = getColumnIndexOrThrow(_stmt, "id")
       val _result: MyEntity
-      if (_cursor.moveToFirst()) {
+      if (_stmt.step()) {
         val _tmpId: String
-        _tmpId = _cursor.getString(_cursorIndexOfId)
+        _tmpId = _stmt.getText(_cursorIndexOfId)
         _result = MyEntity(_tmpId)
       } else {
         error("The query result was empty, but expected a single row to return a NON-NULL object of type <MyEntity>.")
       }
-      return _result
-    } finally {
-      _cursor.close()
-      _statement.release()
+      _result
     }
   }
 
@@ -104,33 +87,26 @@
     appendPlaceholders(_stringBuilder, _inputSize)
     _stringBuilder.append(")")
     val _sql: String = _stringBuilder.toString()
-    val _argCount: Int = 0 + _inputSize
-    val _statement: RoomSQLiteQuery = acquire(_sql, _argCount)
-    var _argIndex: Int = 1
-    for (_item: String? in arg) {
-      if (_item == null) {
-        _statement.bindNull(_argIndex)
-      } else {
-        _statement.bindString(_argIndex, _item)
+    return performReadBlocking(__db, _sql) { _stmt ->
+      var _argIndex: Int = 1
+      for (_item: String? in arg) {
+        if (_item == null) {
+          _stmt.bindNull(_argIndex)
+        } else {
+          _stmt.bindText(_argIndex, _item)
+        }
+        _argIndex++
       }
-      _argIndex++
-    }
-    __db.assertNotSuspendingTransaction()
-    val _cursor: Cursor = query(__db, _statement, false, null)
-    try {
-      val _cursorIndexOfId: Int = getColumnIndexOrThrow(_cursor, "id")
+      val _cursorIndexOfId: Int = getColumnIndexOrThrow(_stmt, "id")
       val _result: MyEntity
-      if (_cursor.moveToFirst()) {
+      if (_stmt.step()) {
         val _tmpId: String
-        _tmpId = _cursor.getString(_cursorIndexOfId)
+        _tmpId = _stmt.getText(_cursorIndexOfId)
         _result = MyEntity(_tmpId)
       } else {
         error("The query result was empty, but expected a single row to return a NON-NULL object of type <MyEntity>.")
       }
-      return _result
-    } finally {
-      _cursor.close()
-      _statement.release()
+      _result
     }
   }
 
@@ -141,29 +117,22 @@
     appendPlaceholders(_stringBuilder, _inputSize)
     _stringBuilder.append(")")
     val _sql: String = _stringBuilder.toString()
-    val _argCount: Int = 0 + _inputSize
-    val _statement: RoomSQLiteQuery = acquire(_sql, _argCount)
-    var _argIndex: Int = 1
-    for (_item: String in arg) {
-      _statement.bindString(_argIndex, _item)
-      _argIndex++
-    }
-    __db.assertNotSuspendingTransaction()
-    val _cursor: Cursor = query(__db, _statement, false, null)
-    try {
-      val _cursorIndexOfId: Int = getColumnIndexOrThrow(_cursor, "id")
+    return performReadBlocking(__db, _sql) { _stmt ->
+      var _argIndex: Int = 1
+      for (_item: String in arg) {
+        _stmt.bindText(_argIndex, _item)
+        _argIndex++
+      }
+      val _cursorIndexOfId: Int = getColumnIndexOrThrow(_stmt, "id")
       val _result: MyEntity
-      if (_cursor.moveToFirst()) {
+      if (_stmt.step()) {
         val _tmpId: String
-        _tmpId = _cursor.getString(_cursorIndexOfId)
+        _tmpId = _stmt.getText(_cursorIndexOfId)
         _result = MyEntity(_tmpId)
       } else {
         error("The query result was empty, but expected a single row to return a NON-NULL object of type <MyEntity>.")
       }
-      return _result
-    } finally {
-      _cursor.close()
-      _statement.release()
+      _result
     }
   }
 
@@ -174,33 +143,26 @@
     appendPlaceholders(_stringBuilder, _inputSize)
     _stringBuilder.append(")")
     val _sql: String = _stringBuilder.toString()
-    val _argCount: Int = 0 + _inputSize
-    val _statement: RoomSQLiteQuery = acquire(_sql, _argCount)
-    var _argIndex: Int = 1
-    for (_item: String? in arg) {
-      if (_item == null) {
-        _statement.bindNull(_argIndex)
-      } else {
-        _statement.bindString(_argIndex, _item)
+    return performReadBlocking(__db, _sql) { _stmt ->
+      var _argIndex: Int = 1
+      for (_item: String? in arg) {
+        if (_item == null) {
+          _stmt.bindNull(_argIndex)
+        } else {
+          _stmt.bindText(_argIndex, _item)
+        }
+        _argIndex++
       }
-      _argIndex++
-    }
-    __db.assertNotSuspendingTransaction()
-    val _cursor: Cursor = query(__db, _statement, false, null)
-    try {
-      val _cursorIndexOfId: Int = getColumnIndexOrThrow(_cursor, "id")
+      val _cursorIndexOfId: Int = getColumnIndexOrThrow(_stmt, "id")
       val _result: MyEntity
-      if (_cursor.moveToFirst()) {
+      if (_stmt.step()) {
         val _tmpId: String
-        _tmpId = _cursor.getString(_cursorIndexOfId)
+        _tmpId = _stmt.getText(_cursorIndexOfId)
         _result = MyEntity(_tmpId)
       } else {
         error("The query result was empty, but expected a single row to return a NON-NULL object of type <MyEntity>.")
       }
-      return _result
-    } finally {
-      _cursor.close()
-      _statement.release()
+      _result
     }
   }
 
@@ -211,29 +173,22 @@
     appendPlaceholders(_stringBuilder, _inputSize)
     _stringBuilder.append(")")
     val _sql: String = _stringBuilder.toString()
-    val _argCount: Int = 0 + _inputSize
-    val _statement: RoomSQLiteQuery = acquire(_sql, _argCount)
-    var _argIndex: Int = 1
-    for (_item: Int in arg) {
-      _statement.bindLong(_argIndex, _item.toLong())
-      _argIndex++
-    }
-    __db.assertNotSuspendingTransaction()
-    val _cursor: Cursor = query(__db, _statement, false, null)
-    try {
-      val _cursorIndexOfId: Int = getColumnIndexOrThrow(_cursor, "id")
+    return performReadBlocking(__db, _sql) { _stmt ->
+      var _argIndex: Int = 1
+      for (_item: Int in arg) {
+        _stmt.bindLong(_argIndex, _item.toLong())
+        _argIndex++
+      }
+      val _cursorIndexOfId: Int = getColumnIndexOrThrow(_stmt, "id")
       val _result: MyEntity
-      if (_cursor.moveToFirst()) {
+      if (_stmt.step()) {
         val _tmpId: String
-        _tmpId = _cursor.getString(_cursorIndexOfId)
+        _tmpId = _stmt.getText(_cursorIndexOfId)
         _result = MyEntity(_tmpId)
       } else {
         error("The query result was empty, but expected a single row to return a NON-NULL object of type <MyEntity>.")
       }
-      return _result
-    } finally {
-      _cursor.close()
-      _statement.release()
+      _result
     }
   }
 
@@ -244,33 +199,26 @@
     appendPlaceholders(_stringBuilder, _inputSize)
     _stringBuilder.append(")")
     val _sql: String = _stringBuilder.toString()
-    val _argCount: Int = 0 + _inputSize
-    val _statement: RoomSQLiteQuery = acquire(_sql, _argCount)
-    var _argIndex: Int = 1
-    if (arg == null) {
-      _statement.bindNull(_argIndex)
-    } else {
-      for (_item: Int in arg) {
-        _statement.bindLong(_argIndex, _item.toLong())
-        _argIndex++
+    return performReadBlocking(__db, _sql) { _stmt ->
+      var _argIndex: Int = 1
+      if (arg == null) {
+        _stmt.bindNull(_argIndex)
+      } else {
+        for (_item: Int in arg) {
+          _stmt.bindLong(_argIndex, _item.toLong())
+          _argIndex++
+        }
       }
-    }
-    __db.assertNotSuspendingTransaction()
-    val _cursor: Cursor = query(__db, _statement, false, null)
-    try {
-      val _cursorIndexOfId: Int = getColumnIndexOrThrow(_cursor, "id")
+      val _cursorIndexOfId: Int = getColumnIndexOrThrow(_stmt, "id")
       val _result: MyEntity
-      if (_cursor.moveToFirst()) {
+      if (_stmt.step()) {
         val _tmpId: String
-        _tmpId = _cursor.getString(_cursorIndexOfId)
+        _tmpId = _stmt.getText(_cursorIndexOfId)
         _result = MyEntity(_tmpId)
       } else {
         error("The query result was empty, but expected a single row to return a NON-NULL object of type <MyEntity>.")
       }
-      return _result
-    } finally {
-      _cursor.close()
-      _statement.release()
+      _result
     }
   }
 
diff --git a/room/room-compiler/src/test/test-data/kotlinCodeGen/basicParameterAdapter_string.kt b/room/room-compiler/src/test/test-data/kotlinCodeGen/basicParameterAdapter_string.kt
index 585fef6..c41869c 100644
--- a/room/room-compiler/src/test/test-data/kotlinCodeGen/basicParameterAdapter_string.kt
+++ b/room/room-compiler/src/test/test-data/kotlinCodeGen/basicParameterAdapter_string.kt
@@ -1,9 +1,6 @@
-import android.database.Cursor
 import androidx.room.RoomDatabase
-import androidx.room.RoomSQLiteQuery
-import androidx.room.RoomSQLiteQuery.Companion.acquire
 import androidx.room.util.getColumnIndexOrThrow
-import androidx.room.util.query
+import androidx.room.util.performReadBlocking
 import javax.`annotation`.processing.Generated
 import kotlin.Int
 import kotlin.String
@@ -24,53 +21,41 @@
 
   public override fun stringParam(arg: String): MyEntity {
     val _sql: String = "SELECT * FROM MyEntity WHERE string = ?"
-    val _statement: RoomSQLiteQuery = acquire(_sql, 1)
-    var _argIndex: Int = 1
-    _statement.bindString(_argIndex, arg)
-    __db.assertNotSuspendingTransaction()
-    val _cursor: Cursor = query(__db, _statement, false, null)
-    try {
-      val _cursorIndexOfString: Int = getColumnIndexOrThrow(_cursor, "string")
+    return performReadBlocking(__db, _sql) { _stmt ->
+      var _argIndex: Int = 1
+      _stmt.bindText(_argIndex, arg)
+      val _cursorIndexOfString: Int = getColumnIndexOrThrow(_stmt, "string")
       val _result: MyEntity
-      if (_cursor.moveToFirst()) {
+      if (_stmt.step()) {
         val _tmpString: String
-        _tmpString = _cursor.getString(_cursorIndexOfString)
+        _tmpString = _stmt.getText(_cursorIndexOfString)
         _result = MyEntity(_tmpString)
       } else {
         error("The query result was empty, but expected a single row to return a NON-NULL object of type <MyEntity>.")
       }
-      return _result
-    } finally {
-      _cursor.close()
-      _statement.release()
+      _result
     }
   }
 
   public override fun nullableStringParam(arg: String?): MyEntity {
     val _sql: String = "SELECT * FROM MyEntity WHERE string = ?"
-    val _statement: RoomSQLiteQuery = acquire(_sql, 1)
-    var _argIndex: Int = 1
-    if (arg == null) {
-      _statement.bindNull(_argIndex)
-    } else {
-      _statement.bindString(_argIndex, arg)
-    }
-    __db.assertNotSuspendingTransaction()
-    val _cursor: Cursor = query(__db, _statement, false, null)
-    try {
-      val _cursorIndexOfString: Int = getColumnIndexOrThrow(_cursor, "string")
+    return performReadBlocking(__db, _sql) { _stmt ->
+      var _argIndex: Int = 1
+      if (arg == null) {
+        _stmt.bindNull(_argIndex)
+      } else {
+        _stmt.bindText(_argIndex, arg)
+      }
+      val _cursorIndexOfString: Int = getColumnIndexOrThrow(_stmt, "string")
       val _result: MyEntity
-      if (_cursor.moveToFirst()) {
+      if (_stmt.step()) {
         val _tmpString: String
-        _tmpString = _cursor.getString(_cursorIndexOfString)
+        _tmpString = _stmt.getText(_cursorIndexOfString)
         _result = MyEntity(_tmpString)
       } else {
         error("The query result was empty, but expected a single row to return a NON-NULL object of type <MyEntity>.")
       }
-      return _result
-    } finally {
-      _cursor.close()
-      _statement.release()
+      _result
     }
   }
 
diff --git a/room/room-compiler/src/test/test-data/kotlinCodeGen/collectionParameterAdapter_string.kt b/room/room-compiler/src/test/test-data/kotlinCodeGen/collectionParameterAdapter_string.kt
index 50fe3b7..73f170b 100644
--- a/room/room-compiler/src/test/test-data/kotlinCodeGen/collectionParameterAdapter_string.kt
+++ b/room/room-compiler/src/test/test-data/kotlinCodeGen/collectionParameterAdapter_string.kt
@@ -1,11 +1,8 @@
-import android.database.Cursor
 import androidx.room.RoomDatabase
-import androidx.room.RoomSQLiteQuery
-import androidx.room.RoomSQLiteQuery.Companion.acquire
 import androidx.room.util.appendPlaceholders
 import androidx.room.util.getColumnIndexOrThrow
 import androidx.room.util.newStringBuilder
-import androidx.room.util.query
+import androidx.room.util.performReadBlocking
 import java.lang.StringBuilder
 import javax.`annotation`.processing.Generated
 import kotlin.Int
@@ -33,29 +30,22 @@
     appendPlaceholders(_stringBuilder, _inputSize)
     _stringBuilder.append(")")
     val _sql: String = _stringBuilder.toString()
-    val _argCount: Int = 0 + _inputSize
-    val _statement: RoomSQLiteQuery = acquire(_sql, _argCount)
-    var _argIndex: Int = 1
-    for (_item: String in arg) {
-      _statement.bindString(_argIndex, _item)
-      _argIndex++
-    }
-    __db.assertNotSuspendingTransaction()
-    val _cursor: Cursor = query(__db, _statement, false, null)
-    try {
-      val _cursorIndexOfString: Int = getColumnIndexOrThrow(_cursor, "string")
+    return performReadBlocking(__db, _sql) { _stmt ->
+      var _argIndex: Int = 1
+      for (_item: String in arg) {
+        _stmt.bindText(_argIndex, _item)
+        _argIndex++
+      }
+      val _cursorIndexOfString: Int = getColumnIndexOrThrow(_stmt, "string")
       val _result: MyEntity
-      if (_cursor.moveToFirst()) {
+      if (_stmt.step()) {
         val _tmpString: String
-        _tmpString = _cursor.getString(_cursorIndexOfString)
+        _tmpString = _stmt.getText(_cursorIndexOfString)
         _result = MyEntity(_tmpString)
       } else {
         error("The query result was empty, but expected a single row to return a NON-NULL object of type <MyEntity>.")
       }
-      return _result
-    } finally {
-      _cursor.close()
-      _statement.release()
+      _result
     }
   }
 
@@ -66,33 +56,26 @@
     appendPlaceholders(_stringBuilder, _inputSize)
     _stringBuilder.append(")")
     val _sql: String = _stringBuilder.toString()
-    val _argCount: Int = 0 + _inputSize
-    val _statement: RoomSQLiteQuery = acquire(_sql, _argCount)
-    var _argIndex: Int = 1
-    if (arg == null) {
-      _statement.bindNull(_argIndex)
-    } else {
-      for (_item: String in arg) {
-        _statement.bindString(_argIndex, _item)
-        _argIndex++
+    return performReadBlocking(__db, _sql) { _stmt ->
+      var _argIndex: Int = 1
+      if (arg == null) {
+        _stmt.bindNull(_argIndex)
+      } else {
+        for (_item: String in arg) {
+          _stmt.bindText(_argIndex, _item)
+          _argIndex++
+        }
       }
-    }
-    __db.assertNotSuspendingTransaction()
-    val _cursor: Cursor = query(__db, _statement, false, null)
-    try {
-      val _cursorIndexOfString: Int = getColumnIndexOrThrow(_cursor, "string")
+      val _cursorIndexOfString: Int = getColumnIndexOrThrow(_stmt, "string")
       val _result: MyEntity
-      if (_cursor.moveToFirst()) {
+      if (_stmt.step()) {
         val _tmpString: String
-        _tmpString = _cursor.getString(_cursorIndexOfString)
+        _tmpString = _stmt.getText(_cursorIndexOfString)
         _result = MyEntity(_tmpString)
       } else {
         error("The query result was empty, but expected a single row to return a NON-NULL object of type <MyEntity>.")
       }
-      return _result
-    } finally {
-      _cursor.close()
-      _statement.release()
+      _result
     }
   }
 
@@ -103,33 +86,26 @@
     appendPlaceholders(_stringBuilder, _inputSize)
     _stringBuilder.append(")")
     val _sql: String = _stringBuilder.toString()
-    val _argCount: Int = 0 + _inputSize
-    val _statement: RoomSQLiteQuery = acquire(_sql, _argCount)
-    var _argIndex: Int = 1
-    for (_item: String? in arg) {
-      if (_item == null) {
-        _statement.bindNull(_argIndex)
-      } else {
-        _statement.bindString(_argIndex, _item)
+    return performReadBlocking(__db, _sql) { _stmt ->
+      var _argIndex: Int = 1
+      for (_item: String? in arg) {
+        if (_item == null) {
+          _stmt.bindNull(_argIndex)
+        } else {
+          _stmt.bindText(_argIndex, _item)
+        }
+        _argIndex++
       }
-      _argIndex++
-    }
-    __db.assertNotSuspendingTransaction()
-    val _cursor: Cursor = query(__db, _statement, false, null)
-    try {
-      val _cursorIndexOfString: Int = getColumnIndexOrThrow(_cursor, "string")
+      val _cursorIndexOfString: Int = getColumnIndexOrThrow(_stmt, "string")
       val _result: MyEntity
-      if (_cursor.moveToFirst()) {
+      if (_stmt.step()) {
         val _tmpString: String
-        _tmpString = _cursor.getString(_cursorIndexOfString)
+        _tmpString = _stmt.getText(_cursorIndexOfString)
         _result = MyEntity(_tmpString)
       } else {
         error("The query result was empty, but expected a single row to return a NON-NULL object of type <MyEntity>.")
       }
-      return _result
-    } finally {
-      _cursor.close()
-      _statement.release()
+      _result
     }
   }
 
@@ -140,29 +116,22 @@
     appendPlaceholders(_stringBuilder, _inputSize)
     _stringBuilder.append(")")
     val _sql: String = _stringBuilder.toString()
-    val _argCount: Int = 0 + _inputSize
-    val _statement: RoomSQLiteQuery = acquire(_sql, _argCount)
-    var _argIndex: Int = 1
-    for (_item: String in arg) {
-      _statement.bindString(_argIndex, _item)
-      _argIndex++
-    }
-    __db.assertNotSuspendingTransaction()
-    val _cursor: Cursor = query(__db, _statement, false, null)
-    try {
-      val _cursorIndexOfString: Int = getColumnIndexOrThrow(_cursor, "string")
+    return performReadBlocking(__db, _sql) { _stmt ->
+      var _argIndex: Int = 1
+      for (_item: String in arg) {
+        _stmt.bindText(_argIndex, _item)
+        _argIndex++
+      }
+      val _cursorIndexOfString: Int = getColumnIndexOrThrow(_stmt, "string")
       val _result: MyEntity
-      if (_cursor.moveToFirst()) {
+      if (_stmt.step()) {
         val _tmpString: String
-        _tmpString = _cursor.getString(_cursorIndexOfString)
+        _tmpString = _stmt.getText(_cursorIndexOfString)
         _result = MyEntity(_tmpString)
       } else {
         error("The query result was empty, but expected a single row to return a NON-NULL object of type <MyEntity>.")
       }
-      return _result
-    } finally {
-      _cursor.close()
-      _statement.release()
+      _result
     }
   }
 
diff --git a/room/room-compiler/src/test/test-data/kotlinCodeGen/database_internalVisibility.kt b/room/room-compiler/src/test/test-data/kotlinCodeGen/database_internalVisibility.kt
index 18b355f..f0b1c65 100644
--- a/room/room-compiler/src/test/test-data/kotlinCodeGen/database_internalVisibility.kt
+++ b/room/room-compiler/src/test/test-data/kotlinCodeGen/database_internalVisibility.kt
@@ -6,10 +6,7 @@
 import androidx.room.util.TableInfo.Companion.read
 import androidx.room.util.dropFtsSyncTriggers
 import androidx.sqlite.SQLiteConnection
-import androidx.sqlite.db.SupportSQLiteDatabase
 import androidx.sqlite.execSQL
-import java.util.HashMap
-import java.util.HashSet
 import javax.`annotation`.processing.Generated
 import kotlin.Any
 import kotlin.Lazy
@@ -63,12 +60,11 @@
 
       public override fun onValidateSchema(connection: SQLiteConnection):
           RoomOpenDelegate.ValidationResult {
-        val _columnsMyEntity: HashMap<String, TableInfo.Column> =
-            HashMap<String, TableInfo.Column>(1)
+        val _columnsMyEntity: MutableMap<String, TableInfo.Column> = mutableMapOf()
         _columnsMyEntity.put("pk", TableInfo.Column("pk", "INTEGER", true, 1, null,
             TableInfo.CREATED_FROM_ENTITY))
-        val _foreignKeysMyEntity: HashSet<TableInfo.ForeignKey> = HashSet<TableInfo.ForeignKey>(0)
-        val _indicesMyEntity: HashSet<TableInfo.Index> = HashSet<TableInfo.Index>(0)
+        val _foreignKeysMyEntity: MutableSet<TableInfo.ForeignKey> = mutableSetOf()
+        val _indicesMyEntity: MutableSet<TableInfo.Index> = mutableSetOf()
         val _infoMyEntity: TableInfo = TableInfo("MyEntity", _columnsMyEntity, _foreignKeysMyEntity,
             _indicesMyEntity)
         val _existingMyEntity: TableInfo = read(connection, "MyEntity")
@@ -88,25 +84,13 @@
   }
 
   protected override fun createInvalidationTracker(): InvalidationTracker {
-    val _shadowTablesMap: HashMap<String, String> = HashMap<String, String>(0)
-    val _viewTables: HashMap<String, Set<String>> = HashMap<String, Set<String>>(0)
+    val _shadowTablesMap: MutableMap<String, String> = mutableMapOf()
+    val _viewTables: MutableMap<String, Set<String>> = mutableMapOf()
     return InvalidationTracker(this, _shadowTablesMap, _viewTables, "MyEntity")
   }
 
   public override fun clearAllTables() {
-    super.assertNotMainThread()
-    val _db: SupportSQLiteDatabase = super.openHelper.writableDatabase
-    try {
-      super.beginTransaction()
-      _db.execSQL("DELETE FROM `MyEntity`")
-      super.setTransactionSuccessful()
-    } finally {
-      super.endTransaction()
-      _db.query("PRAGMA wal_checkpoint(FULL)").close()
-      if (!_db.inTransaction()) {
-        _db.execSQL("VACUUM")
-      }
-    }
+    super.performClear(false, "MyEntity")
   }
 
   protected override fun getRequiredTypeConverterClasses():
diff --git a/room/room-compiler/src/test/test-data/kotlinCodeGen/database_simple.kt b/room/room-compiler/src/test/test-data/kotlinCodeGen/database_simple.kt
index df1e1ac..2b52aea 100644
--- a/room/room-compiler/src/test/test-data/kotlinCodeGen/database_simple.kt
+++ b/room/room-compiler/src/test/test-data/kotlinCodeGen/database_simple.kt
@@ -6,10 +6,7 @@
 import androidx.room.util.TableInfo.Companion.read
 import androidx.room.util.dropFtsSyncTriggers
 import androidx.sqlite.SQLiteConnection
-import androidx.sqlite.db.SupportSQLiteDatabase
 import androidx.sqlite.execSQL
-import java.util.HashMap
-import java.util.HashSet
 import javax.`annotation`.processing.Generated
 import kotlin.Any
 import kotlin.Lazy
@@ -63,12 +60,11 @@
 
       public override fun onValidateSchema(connection: SQLiteConnection):
           RoomOpenDelegate.ValidationResult {
-        val _columnsMyEntity: HashMap<String, TableInfo.Column> =
-            HashMap<String, TableInfo.Column>(1)
+        val _columnsMyEntity: MutableMap<String, TableInfo.Column> = mutableMapOf()
         _columnsMyEntity.put("pk", TableInfo.Column("pk", "INTEGER", true, 1, null,
             TableInfo.CREATED_FROM_ENTITY))
-        val _foreignKeysMyEntity: HashSet<TableInfo.ForeignKey> = HashSet<TableInfo.ForeignKey>(0)
-        val _indicesMyEntity: HashSet<TableInfo.Index> = HashSet<TableInfo.Index>(0)
+        val _foreignKeysMyEntity: MutableSet<TableInfo.ForeignKey> = mutableSetOf()
+        val _indicesMyEntity: MutableSet<TableInfo.Index> = mutableSetOf()
         val _infoMyEntity: TableInfo = TableInfo("MyEntity", _columnsMyEntity, _foreignKeysMyEntity,
             _indicesMyEntity)
         val _existingMyEntity: TableInfo = read(connection, "MyEntity")
@@ -88,25 +84,13 @@
   }
 
   protected override fun createInvalidationTracker(): InvalidationTracker {
-    val _shadowTablesMap: HashMap<String, String> = HashMap<String, String>(0)
-    val _viewTables: HashMap<String, Set<String>> = HashMap<String, Set<String>>(0)
+    val _shadowTablesMap: MutableMap<String, String> = mutableMapOf()
+    val _viewTables: MutableMap<String, Set<String>> = mutableMapOf()
     return InvalidationTracker(this, _shadowTablesMap, _viewTables, "MyEntity")
   }
 
   public override fun clearAllTables() {
-    super.assertNotMainThread()
-    val _db: SupportSQLiteDatabase = super.openHelper.writableDatabase
-    try {
-      super.beginTransaction()
-      _db.execSQL("DELETE FROM `MyEntity`")
-      super.setTransactionSuccessful()
-    } finally {
-      super.endTransaction()
-      _db.query("PRAGMA wal_checkpoint(FULL)").close()
-      if (!_db.inTransaction()) {
-        _db.execSQL("VACUUM")
-      }
-    }
+    super.performClear(false, "MyEntity")
   }
 
   protected override fun getRequiredTypeConverterClasses():
diff --git a/room/room-compiler/src/test/test-data/kotlinCodeGen/database_withFtsAndView.kt b/room/room-compiler/src/test/test-data/kotlinCodeGen/database_withFtsAndView.kt
index 2af2047..558d443 100644
--- a/room/room-compiler/src/test/test-data/kotlinCodeGen/database_withFtsAndView.kt
+++ b/room/room-compiler/src/test/test-data/kotlinCodeGen/database_withFtsAndView.kt
@@ -7,13 +7,9 @@
 import androidx.room.util.ViewInfo
 import androidx.room.util.dropFtsSyncTriggers
 import androidx.sqlite.SQLiteConnection
-import androidx.sqlite.db.SupportSQLiteDatabase
 import androidx.sqlite.execSQL
-import java.util.HashMap
-import java.util.HashSet
 import javax.`annotation`.processing.Generated
 import kotlin.Any
-import kotlin.Boolean
 import kotlin.Lazy
 import kotlin.String
 import kotlin.Suppress
@@ -76,13 +72,11 @@
 
       public override fun onValidateSchema(connection: SQLiteConnection):
           RoomOpenDelegate.ValidationResult {
-        val _columnsMyParentEntity: HashMap<String, TableInfo.Column> =
-            HashMap<String, TableInfo.Column>(1)
+        val _columnsMyParentEntity: MutableMap<String, TableInfo.Column> = mutableMapOf()
         _columnsMyParentEntity.put("parentKey", TableInfo.Column("parentKey", "INTEGER", true, 1,
             null, TableInfo.CREATED_FROM_ENTITY))
-        val _foreignKeysMyParentEntity: HashSet<TableInfo.ForeignKey> =
-            HashSet<TableInfo.ForeignKey>(0)
-        val _indicesMyParentEntity: HashSet<TableInfo.Index> = HashSet<TableInfo.Index>(0)
+        val _foreignKeysMyParentEntity: MutableSet<TableInfo.ForeignKey> = mutableSetOf()
+        val _indicesMyParentEntity: MutableSet<TableInfo.Index> = mutableSetOf()
         val _infoMyParentEntity: TableInfo = TableInfo("MyParentEntity", _columnsMyParentEntity,
             _foreignKeysMyParentEntity, _indicesMyParentEntity)
         val _existingMyParentEntity: TableInfo = tableInfoRead(connection, "MyParentEntity")
@@ -95,16 +89,15 @@
               | Found:
               |""".trimMargin() + _existingMyParentEntity)
         }
-        val _columnsMyEntity: HashMap<String, TableInfo.Column> =
-            HashMap<String, TableInfo.Column>(2)
+        val _columnsMyEntity: MutableMap<String, TableInfo.Column> = mutableMapOf()
         _columnsMyEntity.put("pk", TableInfo.Column("pk", "INTEGER", true, 1, null,
             TableInfo.CREATED_FROM_ENTITY))
         _columnsMyEntity.put("indexedCol", TableInfo.Column("indexedCol", "TEXT", true, 0, null,
             TableInfo.CREATED_FROM_ENTITY))
-        val _foreignKeysMyEntity: HashSet<TableInfo.ForeignKey> = HashSet<TableInfo.ForeignKey>(1)
+        val _foreignKeysMyEntity: MutableSet<TableInfo.ForeignKey> = mutableSetOf()
         _foreignKeysMyEntity.add(TableInfo.ForeignKey("MyParentEntity", "CASCADE", "NO ACTION",
             listOf("indexedCol"), listOf("parentKey")))
-        val _indicesMyEntity: HashSet<TableInfo.Index> = HashSet<TableInfo.Index>(1)
+        val _indicesMyEntity: MutableSet<TableInfo.Index> = mutableSetOf()
         _indicesMyEntity.add(TableInfo.Index("index_MyEntity_indexedCol", false,
             listOf("indexedCol"), listOf("ASC")))
         val _infoMyEntity: TableInfo = TableInfo("MyEntity", _columnsMyEntity, _foreignKeysMyEntity,
@@ -119,7 +112,7 @@
               | Found:
               |""".trimMargin() + _existingMyEntity)
         }
-        val _columnsMyFtsEntity: HashSet<String> = HashSet<String>(2)
+        val _columnsMyFtsEntity: MutableSet<String> = mutableSetOf()
         _columnsMyFtsEntity.add("text")
         val _infoMyFtsEntity: FtsTableInfo = FtsTableInfo("MyFtsEntity", _columnsMyFtsEntity,
             "CREATE VIRTUAL TABLE IF NOT EXISTS `MyFtsEntity` USING FTS4(`text` TEXT NOT NULL)")
@@ -152,43 +145,18 @@
   }
 
   protected override fun createInvalidationTracker(): InvalidationTracker {
-    val _shadowTablesMap: HashMap<String, String> = HashMap<String, String>(1)
+    val _shadowTablesMap: MutableMap<String, String> = mutableMapOf()
     _shadowTablesMap.put("MyFtsEntity", "MyFtsEntity_content")
-    val _viewTables: HashMap<String, Set<String>> = HashMap<String, Set<String>>(1)
-    val _tables: HashSet<String> = HashSet<String>(1)
+    val _viewTables: MutableMap<String, Set<String>> = mutableMapOf()
+    val _tables: MutableSet<String> = mutableSetOf()
     _tables.add("MyFtsEntity")
     _viewTables.put("myview", _tables)
-    return InvalidationTracker(this, _shadowTablesMap, _viewTables,
-        "MyParentEntity","MyEntity","MyFtsEntity")
+    return InvalidationTracker(this, _shadowTablesMap, _viewTables, "MyParentEntity", "MyEntity",
+        "MyFtsEntity")
   }
 
   public override fun clearAllTables() {
-    super.assertNotMainThread()
-    val _db: SupportSQLiteDatabase = super.openHelper.writableDatabase
-    val _supportsDeferForeignKeys: Boolean = android.os.Build.VERSION.SDK_INT >=
-        android.os.Build.VERSION_CODES.LOLLIPOP
-    try {
-      if (!_supportsDeferForeignKeys) {
-        _db.execSQL("PRAGMA foreign_keys = FALSE")
-      }
-      super.beginTransaction()
-      if (_supportsDeferForeignKeys) {
-        _db.execSQL("PRAGMA defer_foreign_keys = TRUE")
-      }
-      _db.execSQL("DELETE FROM `MyParentEntity`")
-      _db.execSQL("DELETE FROM `MyEntity`")
-      _db.execSQL("DELETE FROM `MyFtsEntity`")
-      super.setTransactionSuccessful()
-    } finally {
-      super.endTransaction()
-      if (!_supportsDeferForeignKeys) {
-        _db.execSQL("PRAGMA foreign_keys = TRUE")
-      }
-      _db.query("PRAGMA wal_checkpoint(FULL)").close()
-      if (!_db.inTransaction()) {
-        _db.execSQL("VACUUM")
-      }
-    }
+    super.performClear(true, "MyParentEntity", "MyEntity", "MyFtsEntity")
   }
 
   protected override fun getRequiredTypeConverterClasses():
diff --git a/room/room-compiler/src/test/test-data/kotlinCodeGen/delegatingFunctions_boxedPrimitiveBridge.kt b/room/room-compiler/src/test/test-data/kotlinCodeGen/delegatingFunctions_boxedPrimitiveBridge.kt
index a6820c0..7932177 100644
--- a/room/room-compiler/src/test/test-data/kotlinCodeGen/delegatingFunctions_boxedPrimitiveBridge.kt
+++ b/room/room-compiler/src/test/test-data/kotlinCodeGen/delegatingFunctions_boxedPrimitiveBridge.kt
@@ -1,10 +1,7 @@
-import android.database.Cursor
 import androidx.room.RoomDatabase
-import androidx.room.RoomSQLiteQuery
-import androidx.room.RoomSQLiteQuery.Companion.acquire
 import androidx.room.SharedSQLiteStatement
 import androidx.room.util.getColumnIndexOrThrow
-import androidx.room.util.query
+import androidx.room.util.performReadBlocking
 import androidx.sqlite.db.SupportSQLiteStatement
 import javax.`annotation`.processing.Generated
 import kotlin.Int
@@ -54,25 +51,19 @@
 
   public override fun getEntity(id: Long): MyEntity {
     val _sql: String = "SELECT * FROM MyEntity WHERE pk = ?"
-    val _statement: RoomSQLiteQuery = acquire(_sql, 1)
-    var _argIndex: Int = 1
-    _statement.bindLong(_argIndex, id)
-    __db.assertNotSuspendingTransaction()
-    val _cursor: Cursor = query(__db, _statement, false, null)
-    try {
-      val _cursorIndexOfPk: Int = getColumnIndexOrThrow(_cursor, "pk")
+    return performReadBlocking(__db, _sql) { _stmt ->
+      var _argIndex: Int = 1
+      _stmt.bindLong(_argIndex, id)
+      val _cursorIndexOfPk: Int = getColumnIndexOrThrow(_stmt, "pk")
       val _result: MyEntity
-      if (_cursor.moveToFirst()) {
+      if (_stmt.step()) {
         val _tmpPk: Long
-        _tmpPk = _cursor.getLong(_cursorIndexOfPk)
+        _tmpPk = _stmt.getLong(_cursorIndexOfPk)
         _result = MyEntity(_tmpPk)
       } else {
         error("The query result was empty, but expected a single row to return a NON-NULL object of type <MyEntity>.")
       }
-      return _result
-    } finally {
-      _cursor.close()
-      _statement.release()
+      _result
     }
   }
 
diff --git a/room/room-compiler/src/test/test-data/kotlinCodeGen/delegatingFunctions_defaultImplBridge.kt b/room/room-compiler/src/test/test-data/kotlinCodeGen/delegatingFunctions_defaultImplBridge.kt
index b812ed3..7812585 100644
--- a/room/room-compiler/src/test/test-data/kotlinCodeGen/delegatingFunctions_defaultImplBridge.kt
+++ b/room/room-compiler/src/test/test-data/kotlinCodeGen/delegatingFunctions_defaultImplBridge.kt
@@ -1,9 +1,6 @@
-import android.database.Cursor
 import androidx.room.RoomDatabase
-import androidx.room.RoomSQLiteQuery
-import androidx.room.RoomSQLiteQuery.Companion.acquire
 import androidx.room.util.getColumnIndexOrThrow
-import androidx.room.util.query
+import androidx.room.util.performReadBlocking
 import javax.`annotation`.processing.Generated
 import kotlin.Int
 import kotlin.Long
@@ -25,23 +22,17 @@
 
   public override fun getEntity(): MyEntity {
     val _sql: String = "SELECT * FROM MyEntity"
-    val _statement: RoomSQLiteQuery = acquire(_sql, 0)
-    __db.assertNotSuspendingTransaction()
-    val _cursor: Cursor = query(__db, _statement, false, null)
-    try {
-      val _cursorIndexOfPk: Int = getColumnIndexOrThrow(_cursor, "pk")
+    return performReadBlocking(__db, _sql) { _stmt ->
+      val _cursorIndexOfPk: Int = getColumnIndexOrThrow(_stmt, "pk")
       val _result: MyEntity
-      if (_cursor.moveToFirst()) {
+      if (_stmt.step()) {
         val _tmpPk: Long
-        _tmpPk = _cursor.getLong(_cursorIndexOfPk)
+        _tmpPk = _stmt.getLong(_cursorIndexOfPk)
         _result = MyEntity(_tmpPk)
       } else {
         error("The query result was empty, but expected a single row to return a NON-NULL object of type <MyEntity>.")
       }
-      return _result
-    } finally {
-      _cursor.close()
-      _statement.release()
+      _result
     }
   }
 
diff --git a/room/room-compiler/src/test/test-data/kotlinCodeGen/pojoRowAdapter_boolean.kt b/room/room-compiler/src/test/test-data/kotlinCodeGen/pojoRowAdapter_boolean.kt
index 61f845b..cc819dac 100644
--- a/room/room-compiler/src/test/test-data/kotlinCodeGen/pojoRowAdapter_boolean.kt
+++ b/room/room-compiler/src/test/test-data/kotlinCodeGen/pojoRowAdapter_boolean.kt
@@ -1,10 +1,7 @@
-import android.database.Cursor
 import androidx.room.EntityInsertionAdapter
 import androidx.room.RoomDatabase
-import androidx.room.RoomSQLiteQuery
-import androidx.room.RoomSQLiteQuery.Companion.acquire
 import androidx.room.util.getColumnIndexOrThrow
-import androidx.room.util.query
+import androidx.room.util.performReadBlocking
 import androidx.sqlite.db.SupportSQLiteStatement
 import javax.`annotation`.processing.Generated
 import kotlin.Boolean
@@ -57,37 +54,31 @@
 
   public override fun getEntity(): MyEntity {
     val _sql: String = "SELECT * FROM MyEntity"
-    val _statement: RoomSQLiteQuery = acquire(_sql, 0)
-    __db.assertNotSuspendingTransaction()
-    val _cursor: Cursor = query(__db, _statement, false, null)
-    try {
-      val _cursorIndexOfPk: Int = getColumnIndexOrThrow(_cursor, "pk")
-      val _cursorIndexOfBoolean: Int = getColumnIndexOrThrow(_cursor, "boolean")
-      val _cursorIndexOfNullableBoolean: Int = getColumnIndexOrThrow(_cursor, "nullableBoolean")
+    return performReadBlocking(__db, _sql) { _stmt ->
+      val _cursorIndexOfPk: Int = getColumnIndexOrThrow(_stmt, "pk")
+      val _cursorIndexOfBoolean: Int = getColumnIndexOrThrow(_stmt, "boolean")
+      val _cursorIndexOfNullableBoolean: Int = getColumnIndexOrThrow(_stmt, "nullableBoolean")
       val _result: MyEntity
-      if (_cursor.moveToFirst()) {
+      if (_stmt.step()) {
         val _tmpPk: Int
-        _tmpPk = _cursor.getInt(_cursorIndexOfPk)
+        _tmpPk = _stmt.getLong(_cursorIndexOfPk).toInt()
         val _tmpBoolean: Boolean
         val _tmp: Int
-        _tmp = _cursor.getInt(_cursorIndexOfBoolean)
+        _tmp = _stmt.getLong(_cursorIndexOfBoolean).toInt()
         _tmpBoolean = _tmp != 0
         val _tmpNullableBoolean: Boolean?
         val _tmp_1: Int?
-        if (_cursor.isNull(_cursorIndexOfNullableBoolean)) {
+        if (_stmt.isNull(_cursorIndexOfNullableBoolean)) {
           _tmp_1 = null
         } else {
-          _tmp_1 = _cursor.getInt(_cursorIndexOfNullableBoolean)
+          _tmp_1 = _stmt.getLong(_cursorIndexOfNullableBoolean).toInt()
         }
         _tmpNullableBoolean = _tmp_1?.let { it != 0 }
         _result = MyEntity(_tmpPk,_tmpBoolean,_tmpNullableBoolean)
       } else {
         error("The query result was empty, but expected a single row to return a NON-NULL object of type <MyEntity>.")
       }
-      return _result
-    } finally {
-      _cursor.close()
-      _statement.release()
+      _result
     }
   }
 
diff --git a/room/room-compiler/src/test/test-data/kotlinCodeGen/pojoRowAdapter_byteArray.kt b/room/room-compiler/src/test/test-data/kotlinCodeGen/pojoRowAdapter_byteArray.kt
index e91082b..dac2cad 100644
--- a/room/room-compiler/src/test/test-data/kotlinCodeGen/pojoRowAdapter_byteArray.kt
+++ b/room/room-compiler/src/test/test-data/kotlinCodeGen/pojoRowAdapter_byteArray.kt
@@ -1,10 +1,7 @@
-import android.database.Cursor
 import androidx.room.EntityInsertionAdapter
 import androidx.room.RoomDatabase
-import androidx.room.RoomSQLiteQuery
-import androidx.room.RoomSQLiteQuery.Companion.acquire
 import androidx.room.util.getColumnIndexOrThrow
-import androidx.room.util.query
+import androidx.room.util.performReadBlocking
 import androidx.sqlite.db.SupportSQLiteStatement
 import javax.`annotation`.processing.Generated
 import kotlin.ByteArray
@@ -55,33 +52,27 @@
 
   public override fun getEntity(): MyEntity {
     val _sql: String = "SELECT * FROM MyEntity"
-    val _statement: RoomSQLiteQuery = acquire(_sql, 0)
-    __db.assertNotSuspendingTransaction()
-    val _cursor: Cursor = query(__db, _statement, false, null)
-    try {
-      val _cursorIndexOfPk: Int = getColumnIndexOrThrow(_cursor, "pk")
-      val _cursorIndexOfByteArray: Int = getColumnIndexOrThrow(_cursor, "byteArray")
-      val _cursorIndexOfNullableByteArray: Int = getColumnIndexOrThrow(_cursor, "nullableByteArray")
+    return performReadBlocking(__db, _sql) { _stmt ->
+      val _cursorIndexOfPk: Int = getColumnIndexOrThrow(_stmt, "pk")
+      val _cursorIndexOfByteArray: Int = getColumnIndexOrThrow(_stmt, "byteArray")
+      val _cursorIndexOfNullableByteArray: Int = getColumnIndexOrThrow(_stmt, "nullableByteArray")
       val _result: MyEntity
-      if (_cursor.moveToFirst()) {
+      if (_stmt.step()) {
         val _tmpPk: Int
-        _tmpPk = _cursor.getInt(_cursorIndexOfPk)
+        _tmpPk = _stmt.getLong(_cursorIndexOfPk).toInt()
         val _tmpByteArray: ByteArray
-        _tmpByteArray = _cursor.getBlob(_cursorIndexOfByteArray)
+        _tmpByteArray = _stmt.getBlob(_cursorIndexOfByteArray)
         val _tmpNullableByteArray: ByteArray?
-        if (_cursor.isNull(_cursorIndexOfNullableByteArray)) {
+        if (_stmt.isNull(_cursorIndexOfNullableByteArray)) {
           _tmpNullableByteArray = null
         } else {
-          _tmpNullableByteArray = _cursor.getBlob(_cursorIndexOfNullableByteArray)
+          _tmpNullableByteArray = _stmt.getBlob(_cursorIndexOfNullableByteArray)
         }
         _result = MyEntity(_tmpPk,_tmpByteArray,_tmpNullableByteArray)
       } else {
         error("The query result was empty, but expected a single row to return a NON-NULL object of type <MyEntity>.")
       }
-      return _result
-    } finally {
-      _cursor.close()
-      _statement.release()
+      _result
     }
   }
 
diff --git a/room/room-compiler/src/test/test-data/kotlinCodeGen/pojoRowAdapter_customTypeConverter.kt b/room/room-compiler/src/test/test-data/kotlinCodeGen/pojoRowAdapter_customTypeConverter.kt
index 563454a3..a5f784b 100644
--- a/room/room-compiler/src/test/test-data/kotlinCodeGen/pojoRowAdapter_customTypeConverter.kt
+++ b/room/room-compiler/src/test/test-data/kotlinCodeGen/pojoRowAdapter_customTypeConverter.kt
@@ -1,10 +1,7 @@
-import android.database.Cursor
 import androidx.room.EntityInsertionAdapter
 import androidx.room.RoomDatabase
-import androidx.room.RoomSQLiteQuery
-import androidx.room.RoomSQLiteQuery.Companion.acquire
 import androidx.room.util.getColumnIndexOrThrow
-import androidx.room.util.query
+import androidx.room.util.performReadBlocking
 import androidx.sqlite.db.SupportSQLiteStatement
 import javax.`annotation`.processing.Generated
 import kotlin.Int
@@ -51,28 +48,22 @@
 
   public override fun getEntity(): MyEntity {
     val _sql: String = "SELECT * FROM MyEntity"
-    val _statement: RoomSQLiteQuery = acquire(_sql, 0)
-    __db.assertNotSuspendingTransaction()
-    val _cursor: Cursor = query(__db, _statement, false, null)
-    try {
-      val _cursorIndexOfPk: Int = getColumnIndexOrThrow(_cursor, "pk")
-      val _cursorIndexOfFoo: Int = getColumnIndexOrThrow(_cursor, "foo")
+    return performReadBlocking(__db, _sql) { _stmt ->
+      val _cursorIndexOfPk: Int = getColumnIndexOrThrow(_stmt, "pk")
+      val _cursorIndexOfFoo: Int = getColumnIndexOrThrow(_stmt, "foo")
       val _result: MyEntity
-      if (_cursor.moveToFirst()) {
+      if (_stmt.step()) {
         val _tmpPk: Int
-        _tmpPk = _cursor.getInt(_cursorIndexOfPk)
+        _tmpPk = _stmt.getLong(_cursorIndexOfPk).toInt()
         val _tmpFoo: Foo
         val _tmp: String
-        _tmp = _cursor.getString(_cursorIndexOfFoo)
+        _tmp = _stmt.getText(_cursorIndexOfFoo)
         _tmpFoo = __fooConverter.fromString(_tmp)
         _result = MyEntity(_tmpPk,_tmpFoo)
       } else {
         error("The query result was empty, but expected a single row to return a NON-NULL object of type <MyEntity>.")
       }
-      return _result
-    } finally {
-      _cursor.close()
-      _statement.release()
+      _result
     }
   }
 
diff --git a/room/room-compiler/src/test/test-data/kotlinCodeGen/pojoRowAdapter_customTypeConverter_composite.kt b/room/room-compiler/src/test/test-data/kotlinCodeGen/pojoRowAdapter_customTypeConverter_composite.kt
index 5859adc..1a6dc11 100644
--- a/room/room-compiler/src/test/test-data/kotlinCodeGen/pojoRowAdapter_customTypeConverter_composite.kt
+++ b/room/room-compiler/src/test/test-data/kotlinCodeGen/pojoRowAdapter_customTypeConverter_composite.kt
@@ -1,10 +1,7 @@
-import android.database.Cursor
 import androidx.room.EntityInsertionAdapter
 import androidx.room.RoomDatabase
-import androidx.room.RoomSQLiteQuery
-import androidx.room.RoomSQLiteQuery.Companion.acquire
 import androidx.room.util.getColumnIndexOrThrow
-import androidx.room.util.query
+import androidx.room.util.performReadBlocking
 import androidx.sqlite.db.SupportSQLiteStatement
 import javax.`annotation`.processing.Generated
 import kotlin.Int
@@ -50,29 +47,23 @@
 
   public override fun getEntity(): MyEntity {
     val _sql: String = "SELECT * FROM MyEntity"
-    val _statement: RoomSQLiteQuery = acquire(_sql, 0)
-    __db.assertNotSuspendingTransaction()
-    val _cursor: Cursor = query(__db, _statement, false, null)
-    try {
-      val _cursorIndexOfPk: Int = getColumnIndexOrThrow(_cursor, "pk")
-      val _cursorIndexOfBar: Int = getColumnIndexOrThrow(_cursor, "bar")
+    return performReadBlocking(__db, _sql) { _stmt ->
+      val _cursorIndexOfPk: Int = getColumnIndexOrThrow(_stmt, "pk")
+      val _cursorIndexOfBar: Int = getColumnIndexOrThrow(_stmt, "bar")
       val _result: MyEntity
-      if (_cursor.moveToFirst()) {
+      if (_stmt.step()) {
         val _tmpPk: Int
-        _tmpPk = _cursor.getInt(_cursorIndexOfPk)
+        _tmpPk = _stmt.getLong(_cursorIndexOfPk).toInt()
         val _tmpBar: Bar
         val _tmp: String
-        _tmp = _cursor.getString(_cursorIndexOfBar)
+        _tmp = _stmt.getText(_cursorIndexOfBar)
         val _tmp_1: Foo = FooBarConverter.fromString(_tmp)
         _tmpBar = FooBarConverter.fromFoo(_tmp_1)
         _result = MyEntity(_tmpPk,_tmpBar)
       } else {
         error("The query result was empty, but expected a single row to return a NON-NULL object of type <MyEntity>.")
       }
-      return _result
-    } finally {
-      _cursor.close()
-      _statement.release()
+      _result
     }
   }
 
diff --git a/room/room-compiler/src/test/test-data/kotlinCodeGen/pojoRowAdapter_customTypeConverter_internalVisibility.kt b/room/room-compiler/src/test/test-data/kotlinCodeGen/pojoRowAdapter_customTypeConverter_internalVisibility.kt
index 69a1e7a..a5fdc7d 100644
--- a/room/room-compiler/src/test/test-data/kotlinCodeGen/pojoRowAdapter_customTypeConverter_internalVisibility.kt
+++ b/room/room-compiler/src/test/test-data/kotlinCodeGen/pojoRowAdapter_customTypeConverter_internalVisibility.kt
@@ -1,10 +1,7 @@
-import android.database.Cursor
 import androidx.room.EntityInsertionAdapter
 import androidx.room.RoomDatabase
-import androidx.room.RoomSQLiteQuery
-import androidx.room.RoomSQLiteQuery.Companion.acquire
 import androidx.room.util.getColumnIndexOrThrow
-import androidx.room.util.query
+import androidx.room.util.performReadBlocking
 import androidx.sqlite.db.SupportSQLiteStatement
 import javax.`annotation`.processing.Generated
 import kotlin.Int
@@ -51,28 +48,22 @@
 
   internal override fun getEntity(): MyEntity {
     val _sql: String = "SELECT * FROM MyEntity"
-    val _statement: RoomSQLiteQuery = acquire(_sql, 0)
-    __db.assertNotSuspendingTransaction()
-    val _cursor: Cursor = query(__db, _statement, false, null)
-    try {
-      val _cursorIndexOfPk: Int = getColumnIndexOrThrow(_cursor, "pk")
-      val _cursorIndexOfFoo: Int = getColumnIndexOrThrow(_cursor, "foo")
+    return performReadBlocking(__db, _sql) { _stmt ->
+      val _cursorIndexOfPk: Int = getColumnIndexOrThrow(_stmt, "pk")
+      val _cursorIndexOfFoo: Int = getColumnIndexOrThrow(_stmt, "foo")
       val _result: MyEntity
-      if (_cursor.moveToFirst()) {
+      if (_stmt.step()) {
         val _tmpPk: Int
-        _tmpPk = _cursor.getInt(_cursorIndexOfPk)
+        _tmpPk = _stmt.getLong(_cursorIndexOfPk).toInt()
         val _tmpFoo: Foo
         val _tmp: String
-        _tmp = _cursor.getString(_cursorIndexOfFoo)
+        _tmp = _stmt.getText(_cursorIndexOfFoo)
         _tmpFoo = __fooConverter.fromString(_tmp)
         _result = MyEntity(_tmpPk,_tmpFoo)
       } else {
         error("The query result was empty, but expected a single row to return a NON-NULL object of type <MyEntity>.")
       }
-      return _result
-    } finally {
-      _cursor.close()
-      _statement.release()
+      _result
     }
   }
 
diff --git a/room/room-compiler/src/test/test-data/kotlinCodeGen/pojoRowAdapter_customTypeConverter_nullAware.kt b/room/room-compiler/src/test/test-data/kotlinCodeGen/pojoRowAdapter_customTypeConverter_nullAware.kt
index 7eef3c0..c23234e 100644
--- a/room/room-compiler/src/test/test-data/kotlinCodeGen/pojoRowAdapter_customTypeConverter_nullAware.kt
+++ b/room/room-compiler/src/test/test-data/kotlinCodeGen/pojoRowAdapter_customTypeConverter_nullAware.kt
@@ -1,10 +1,7 @@
-import android.database.Cursor
 import androidx.room.EntityInsertionAdapter
 import androidx.room.RoomDatabase
-import androidx.room.RoomSQLiteQuery
-import androidx.room.RoomSQLiteQuery.Companion.acquire
 import androidx.room.util.getColumnIndexOrThrow
-import androidx.room.util.query
+import androidx.room.util.performReadBlocking
 import androidx.sqlite.db.SupportSQLiteStatement
 import javax.`annotation`.processing.Generated
 import kotlin.Int
@@ -60,23 +57,20 @@
 
   public override fun getEntity(): MyEntity {
     val _sql: String = "SELECT * FROM MyEntity"
-    val _statement: RoomSQLiteQuery = acquire(_sql, 0)
-    __db.assertNotSuspendingTransaction()
-    val _cursor: Cursor = query(__db, _statement, false, null)
-    try {
-      val _cursorIndexOfPk: Int = getColumnIndexOrThrow(_cursor, "pk")
-      val _cursorIndexOfFoo: Int = getColumnIndexOrThrow(_cursor, "foo")
-      val _cursorIndexOfBar: Int = getColumnIndexOrThrow(_cursor, "bar")
+    return performReadBlocking(__db, _sql) { _stmt ->
+      val _cursorIndexOfPk: Int = getColumnIndexOrThrow(_stmt, "pk")
+      val _cursorIndexOfFoo: Int = getColumnIndexOrThrow(_stmt, "foo")
+      val _cursorIndexOfBar: Int = getColumnIndexOrThrow(_stmt, "bar")
       val _result: MyEntity
-      if (_cursor.moveToFirst()) {
+      if (_stmt.step()) {
         val _tmpPk: Int
-        _tmpPk = _cursor.getInt(_cursorIndexOfPk)
+        _tmpPk = _stmt.getLong(_cursorIndexOfPk).toInt()
         val _tmpFoo: Foo
         val _tmp: String?
-        if (_cursor.isNull(_cursorIndexOfFoo)) {
+        if (_stmt.isNull(_cursorIndexOfFoo)) {
           _tmp = null
         } else {
-          _tmp = _cursor.getString(_cursorIndexOfFoo)
+          _tmp = _stmt.getText(_cursorIndexOfFoo)
         }
         val _tmp_1: Foo? = FooBarConverter.fromString(_tmp)
         if (_tmp_1 == null) {
@@ -86,10 +80,10 @@
         }
         val _tmpBar: Bar
         val _tmp_2: String?
-        if (_cursor.isNull(_cursorIndexOfBar)) {
+        if (_stmt.isNull(_cursorIndexOfBar)) {
           _tmp_2 = null
         } else {
-          _tmp_2 = _cursor.getString(_cursorIndexOfBar)
+          _tmp_2 = _stmt.getText(_cursorIndexOfBar)
         }
         val _tmp_3: Foo? = FooBarConverter.fromString(_tmp_2)
         val _tmp_4: Bar?
@@ -107,10 +101,7 @@
       } else {
         error("The query result was empty, but expected a single row to return a NON-NULL object of type <MyEntity>.")
       }
-      return _result
-    } finally {
-      _cursor.close()
-      _statement.release()
+      _result
     }
   }
 
diff --git a/room/room-compiler/src/test/test-data/kotlinCodeGen/pojoRowAdapter_customTypeConverter_provided.kt b/room/room-compiler/src/test/test-data/kotlinCodeGen/pojoRowAdapter_customTypeConverter_provided.kt
index e3f3b20..e10ea35 100644
--- a/room/room-compiler/src/test/test-data/kotlinCodeGen/pojoRowAdapter_customTypeConverter_provided.kt
+++ b/room/room-compiler/src/test/test-data/kotlinCodeGen/pojoRowAdapter_customTypeConverter_provided.kt
@@ -1,10 +1,7 @@
-import android.database.Cursor
 import androidx.room.EntityInsertionAdapter
 import androidx.room.RoomDatabase
-import androidx.room.RoomSQLiteQuery
-import androidx.room.RoomSQLiteQuery.Companion.acquire
 import androidx.room.util.getColumnIndexOrThrow
-import androidx.room.util.query
+import androidx.room.util.performReadBlocking
 import androidx.sqlite.db.SupportSQLiteStatement
 import javax.`annotation`.processing.Generated
 import kotlin.Int
@@ -55,28 +52,22 @@
 
   public override fun getEntity(): MyEntity {
     val _sql: String = "SELECT * FROM MyEntity"
-    val _statement: RoomSQLiteQuery = acquire(_sql, 0)
-    __db.assertNotSuspendingTransaction()
-    val _cursor: Cursor = query(__db, _statement, false, null)
-    try {
-      val _cursorIndexOfPk: Int = getColumnIndexOrThrow(_cursor, "pk")
-      val _cursorIndexOfFoo: Int = getColumnIndexOrThrow(_cursor, "foo")
+    return performReadBlocking(__db, _sql) { _stmt ->
+      val _cursorIndexOfPk: Int = getColumnIndexOrThrow(_stmt, "pk")
+      val _cursorIndexOfFoo: Int = getColumnIndexOrThrow(_stmt, "foo")
       val _result: MyEntity
-      if (_cursor.moveToFirst()) {
+      if (_stmt.step()) {
         val _tmpPk: Int
-        _tmpPk = _cursor.getInt(_cursorIndexOfPk)
+        _tmpPk = _stmt.getLong(_cursorIndexOfPk).toInt()
         val _tmpFoo: Foo
         val _tmp: String
-        _tmp = _cursor.getString(_cursorIndexOfFoo)
+        _tmp = _stmt.getText(_cursorIndexOfFoo)
         _tmpFoo = __fooConverter().fromString(_tmp)
         _result = MyEntity(_tmpPk,_tmpFoo)
       } else {
         error("The query result was empty, but expected a single row to return a NON-NULL object of type <MyEntity>.")
       }
-      return _result
-    } finally {
-      _cursor.close()
-      _statement.release()
+      _result
     }
   }
 
diff --git a/room/room-compiler/src/test/test-data/kotlinCodeGen/pojoRowAdapter_embedded.kt b/room/room-compiler/src/test/test-data/kotlinCodeGen/pojoRowAdapter_embedded.kt
index c76f721..98b4588 100644
--- a/room/room-compiler/src/test/test-data/kotlinCodeGen/pojoRowAdapter_embedded.kt
+++ b/room/room-compiler/src/test/test-data/kotlinCodeGen/pojoRowAdapter_embedded.kt
@@ -1,10 +1,7 @@
-import android.database.Cursor
 import androidx.room.EntityInsertionAdapter
 import androidx.room.RoomDatabase
-import androidx.room.RoomSQLiteQuery
-import androidx.room.RoomSQLiteQuery.Companion.acquire
 import androidx.room.util.getColumnIndexOrThrow
-import androidx.room.util.query
+import androidx.room.util.performReadBlocking
 import androidx.sqlite.db.SupportSQLiteStatement
 import javax.`annotation`.processing.Generated
 import kotlin.Int
@@ -59,32 +56,29 @@
 
   public override fun getEntity(): MyEntity {
     val _sql: String = "SELECT * FROM MyEntity"
-    val _statement: RoomSQLiteQuery = acquire(_sql, 0)
-    __db.assertNotSuspendingTransaction()
-    val _cursor: Cursor = query(__db, _statement, false, null)
-    try {
-      val _cursorIndexOfPk: Int = getColumnIndexOrThrow(_cursor, "pk")
-      val _cursorIndexOfNumberData: Int = getColumnIndexOrThrow(_cursor, "numberData")
-      val _cursorIndexOfStringData: Int = getColumnIndexOrThrow(_cursor, "stringData")
-      val _cursorIndexOfNumberData_1: Int = getColumnIndexOrThrow(_cursor, "nullablenumberData")
-      val _cursorIndexOfStringData_1: Int = getColumnIndexOrThrow(_cursor, "nullablestringData")
+    return performReadBlocking(__db, _sql) { _stmt ->
+      val _cursorIndexOfPk: Int = getColumnIndexOrThrow(_stmt, "pk")
+      val _cursorIndexOfNumberData: Int = getColumnIndexOrThrow(_stmt, "numberData")
+      val _cursorIndexOfStringData: Int = getColumnIndexOrThrow(_stmt, "stringData")
+      val _cursorIndexOfNumberData_1: Int = getColumnIndexOrThrow(_stmt, "nullablenumberData")
+      val _cursorIndexOfStringData_1: Int = getColumnIndexOrThrow(_stmt, "nullablestringData")
       val _result: MyEntity
-      if (_cursor.moveToFirst()) {
+      if (_stmt.step()) {
         val _tmpPk: Int
-        _tmpPk = _cursor.getInt(_cursorIndexOfPk)
+        _tmpPk = _stmt.getLong(_cursorIndexOfPk).toInt()
         val _tmpFoo: Foo
         val _tmpNumberData: Long
-        _tmpNumberData = _cursor.getLong(_cursorIndexOfNumberData)
+        _tmpNumberData = _stmt.getLong(_cursorIndexOfNumberData)
         val _tmpStringData: String
-        _tmpStringData = _cursor.getString(_cursorIndexOfStringData)
+        _tmpStringData = _stmt.getText(_cursorIndexOfStringData)
         _tmpFoo = Foo(_tmpNumberData,_tmpStringData)
         val _tmpNullableFoo: Foo?
-        if (!(_cursor.isNull(_cursorIndexOfNumberData_1) &&
-            _cursor.isNull(_cursorIndexOfStringData_1))) {
+        if (!(_stmt.isNull(_cursorIndexOfNumberData_1) &&
+            _stmt.isNull(_cursorIndexOfStringData_1))) {
           val _tmpNumberData_1: Long
-          _tmpNumberData_1 = _cursor.getLong(_cursorIndexOfNumberData_1)
+          _tmpNumberData_1 = _stmt.getLong(_cursorIndexOfNumberData_1)
           val _tmpStringData_1: String
-          _tmpStringData_1 = _cursor.getString(_cursorIndexOfStringData_1)
+          _tmpStringData_1 = _stmt.getText(_cursorIndexOfStringData_1)
           _tmpNullableFoo = Foo(_tmpNumberData_1,_tmpStringData_1)
         } else {
           _tmpNullableFoo = null
@@ -93,10 +87,7 @@
       } else {
         error("The query result was empty, but expected a single row to return a NON-NULL object of type <MyEntity>.")
       }
-      return _result
-    } finally {
-      _cursor.close()
-      _statement.release()
+      _result
     }
   }
 
diff --git a/room/room-compiler/src/test/test-data/kotlinCodeGen/pojoRowAdapter_enum.kt b/room/room-compiler/src/test/test-data/kotlinCodeGen/pojoRowAdapter_enum.kt
index cbb1148..c83cdf9 100644
--- a/room/room-compiler/src/test/test-data/kotlinCodeGen/pojoRowAdapter_enum.kt
+++ b/room/room-compiler/src/test/test-data/kotlinCodeGen/pojoRowAdapter_enum.kt
@@ -1,10 +1,7 @@
-import android.database.Cursor
 import androidx.room.EntityInsertionAdapter
 import androidx.room.RoomDatabase
-import androidx.room.RoomSQLiteQuery
-import androidx.room.RoomSQLiteQuery.Companion.acquire
 import androidx.room.util.getColumnIndexOrThrow
-import androidx.room.util.query
+import androidx.room.util.performReadBlocking
 import androidx.sqlite.db.SupportSQLiteStatement
 import java.lang.IllegalArgumentException
 import javax.`annotation`.processing.Generated
@@ -55,33 +52,27 @@
 
   public override fun getEntity(): MyEntity {
     val _sql: String = "SELECT * FROM MyEntity"
-    val _statement: RoomSQLiteQuery = acquire(_sql, 0)
-    __db.assertNotSuspendingTransaction()
-    val _cursor: Cursor = query(__db, _statement, false, null)
-    try {
-      val _cursorIndexOfPk: Int = getColumnIndexOrThrow(_cursor, "pk")
-      val _cursorIndexOfEnum: Int = getColumnIndexOrThrow(_cursor, "enum")
-      val _cursorIndexOfNullableEnum: Int = getColumnIndexOrThrow(_cursor, "nullableEnum")
+    return performReadBlocking(__db, _sql) { _stmt ->
+      val _cursorIndexOfPk: Int = getColumnIndexOrThrow(_stmt, "pk")
+      val _cursorIndexOfEnum: Int = getColumnIndexOrThrow(_stmt, "enum")
+      val _cursorIndexOfNullableEnum: Int = getColumnIndexOrThrow(_stmt, "nullableEnum")
       val _result: MyEntity
-      if (_cursor.moveToFirst()) {
+      if (_stmt.step()) {
         val _tmpPk: Int
-        _tmpPk = _cursor.getInt(_cursorIndexOfPk)
+        _tmpPk = _stmt.getLong(_cursorIndexOfPk).toInt()
         val _tmpEnum: Fruit
-        _tmpEnum = __Fruit_stringToEnum(_cursor.getString(_cursorIndexOfEnum))
+        _tmpEnum = __Fruit_stringToEnum(_stmt.getText(_cursorIndexOfEnum))
         val _tmpNullableEnum: Fruit?
-        if (_cursor.isNull(_cursorIndexOfNullableEnum)) {
+        if (_stmt.isNull(_cursorIndexOfNullableEnum)) {
           _tmpNullableEnum = null
         } else {
-          _tmpNullableEnum = __Fruit_stringToEnum(_cursor.getString(_cursorIndexOfNullableEnum))
+          _tmpNullableEnum = __Fruit_stringToEnum(_stmt.getText(_cursorIndexOfNullableEnum))
         }
         _result = MyEntity(_tmpPk,_tmpEnum,_tmpNullableEnum)
       } else {
         error("The query result was empty, but expected a single row to return a NON-NULL object of type <MyEntity>.")
       }
-      return _result
-    } finally {
-      _cursor.close()
-      _statement.release()
+      _result
     }
   }
 
diff --git a/room/room-compiler/src/test/test-data/kotlinCodeGen/pojoRowAdapter_internalVisibility.kt b/room/room-compiler/src/test/test-data/kotlinCodeGen/pojoRowAdapter_internalVisibility.kt
index 9ada137..fa0b92ae 100644
--- a/room/room-compiler/src/test/test-data/kotlinCodeGen/pojoRowAdapter_internalVisibility.kt
+++ b/room/room-compiler/src/test/test-data/kotlinCodeGen/pojoRowAdapter_internalVisibility.kt
@@ -1,10 +1,7 @@
-import android.database.Cursor
 import androidx.room.EntityInsertionAdapter
 import androidx.room.RoomDatabase
-import androidx.room.RoomSQLiteQuery
-import androidx.room.RoomSQLiteQuery.Companion.acquire
 import androidx.room.util.getColumnIndexOrThrow
-import androidx.room.util.query
+import androidx.room.util.performReadBlocking
 import androidx.sqlite.db.SupportSQLiteStatement
 import javax.`annotation`.processing.Generated
 import kotlin.Int
@@ -51,30 +48,24 @@
 
   public override fun getEntity(): MyEntity {
     val _sql: String = "SELECT * FROM MyEntity"
-    val _statement: RoomSQLiteQuery = acquire(_sql, 0)
-    __db.assertNotSuspendingTransaction()
-    val _cursor: Cursor = query(__db, _statement, false, null)
-    try {
-      val _cursorIndexOfPk: Int = getColumnIndexOrThrow(_cursor, "pk")
-      val _cursorIndexOfInternalVal: Int = getColumnIndexOrThrow(_cursor, "internalVal")
-      val _cursorIndexOfInternalVar: Int = getColumnIndexOrThrow(_cursor, "internalVar")
-      val _cursorIndexOfInternalSetterVar: Int = getColumnIndexOrThrow(_cursor, "internalSetterVar")
+    return performReadBlocking(__db, _sql) { _stmt ->
+      val _cursorIndexOfPk: Int = getColumnIndexOrThrow(_stmt, "pk")
+      val _cursorIndexOfInternalVal: Int = getColumnIndexOrThrow(_stmt, "internalVal")
+      val _cursorIndexOfInternalVar: Int = getColumnIndexOrThrow(_stmt, "internalVar")
+      val _cursorIndexOfInternalSetterVar: Int = getColumnIndexOrThrow(_stmt, "internalSetterVar")
       val _result: MyEntity
-      if (_cursor.moveToFirst()) {
+      if (_stmt.step()) {
         val _tmpPk: Int
-        _tmpPk = _cursor.getInt(_cursorIndexOfPk)
+        _tmpPk = _stmt.getLong(_cursorIndexOfPk).toInt()
         val _tmpInternalVal: Long
-        _tmpInternalVal = _cursor.getLong(_cursorIndexOfInternalVal)
+        _tmpInternalVal = _stmt.getLong(_cursorIndexOfInternalVal)
         _result = MyEntity(_tmpPk,_tmpInternalVal)
-        _result.internalVar = _cursor.getLong(_cursorIndexOfInternalVar)
-        _result.internalSetterVar = _cursor.getLong(_cursorIndexOfInternalSetterVar)
+        _result.internalVar = _stmt.getLong(_cursorIndexOfInternalVar)
+        _result.internalSetterVar = _stmt.getLong(_cursorIndexOfInternalSetterVar)
       } else {
         error("The query result was empty, but expected a single row to return a NON-NULL object of type <MyEntity>.")
       }
-      return _result
-    } finally {
-      _cursor.close()
-      _statement.release()
+      _result
     }
   }
 
diff --git a/room/room-compiler/src/test/test-data/kotlinCodeGen/pojoRowAdapter_otherModule.kt b/room/room-compiler/src/test/test-data/kotlinCodeGen/pojoRowAdapter_otherModule.kt
index 92db9a1..41882d5 100644
--- a/room/room-compiler/src/test/test-data/kotlinCodeGen/pojoRowAdapter_otherModule.kt
+++ b/room/room-compiler/src/test/test-data/kotlinCodeGen/pojoRowAdapter_otherModule.kt
@@ -1,10 +1,7 @@
-import android.database.Cursor
 import androidx.room.EntityInsertionAdapter
 import androidx.room.RoomDatabase
-import androidx.room.RoomSQLiteQuery
-import androidx.room.RoomSQLiteQuery.Companion.acquire
 import androidx.room.util.getColumnIndexOrThrow
-import androidx.room.util.query
+import androidx.room.util.performReadBlocking
 import androidx.sqlite.db.SupportSQLiteStatement
 import javax.`annotation`.processing.Generated
 import kotlin.Int
@@ -78,70 +75,64 @@
 
   public override fun getEntity(): MyEntity {
     val _sql: String = "SELECT * FROM MyEntity"
-    val _statement: RoomSQLiteQuery = acquire(_sql, 0)
-    __db.assertNotSuspendingTransaction()
-    val _cursor: Cursor = query(__db, _statement, false, null)
-    try {
-      val _cursorIndexOfPk: Int = getColumnIndexOrThrow(_cursor, "pk")
-      val _cursorIndexOfPrimitive: Int = getColumnIndexOrThrow(_cursor, "primitive")
-      val _cursorIndexOfString: Int = getColumnIndexOrThrow(_cursor, "string")
-      val _cursorIndexOfNullableString: Int = getColumnIndexOrThrow(_cursor, "nullableString")
-      val _cursorIndexOfFieldString: Int = getColumnIndexOrThrow(_cursor, "fieldString")
-      val _cursorIndexOfNullableFieldString: Int = getColumnIndexOrThrow(_cursor,
+    return performReadBlocking(__db, _sql) { _stmt ->
+      val _cursorIndexOfPk: Int = getColumnIndexOrThrow(_stmt, "pk")
+      val _cursorIndexOfPrimitive: Int = getColumnIndexOrThrow(_stmt, "primitive")
+      val _cursorIndexOfString: Int = getColumnIndexOrThrow(_stmt, "string")
+      val _cursorIndexOfNullableString: Int = getColumnIndexOrThrow(_stmt, "nullableString")
+      val _cursorIndexOfFieldString: Int = getColumnIndexOrThrow(_stmt, "fieldString")
+      val _cursorIndexOfNullableFieldString: Int = getColumnIndexOrThrow(_stmt,
           "nullableFieldString")
-      val _cursorIndexOfVariablePrimitive: Int = getColumnIndexOrThrow(_cursor, "variablePrimitive")
-      val _cursorIndexOfVariableString: Int = getColumnIndexOrThrow(_cursor, "variableString")
-      val _cursorIndexOfVariableNullableString: Int = getColumnIndexOrThrow(_cursor,
+      val _cursorIndexOfVariablePrimitive: Int = getColumnIndexOrThrow(_stmt, "variablePrimitive")
+      val _cursorIndexOfVariableString: Int = getColumnIndexOrThrow(_stmt, "variableString")
+      val _cursorIndexOfVariableNullableString: Int = getColumnIndexOrThrow(_stmt,
           "variableNullableString")
-      val _cursorIndexOfVariableFieldString: Int = getColumnIndexOrThrow(_cursor,
+      val _cursorIndexOfVariableFieldString: Int = getColumnIndexOrThrow(_stmt,
           "variableFieldString")
-      val _cursorIndexOfVariableNullableFieldString: Int = getColumnIndexOrThrow(_cursor,
+      val _cursorIndexOfVariableNullableFieldString: Int = getColumnIndexOrThrow(_stmt,
           "variableNullableFieldString")
       val _result: MyEntity
-      if (_cursor.moveToFirst()) {
+      if (_stmt.step()) {
         val _tmpPk: Int
-        _tmpPk = _cursor.getInt(_cursorIndexOfPk)
+        _tmpPk = _stmt.getLong(_cursorIndexOfPk).toInt()
         val _tmpPrimitive: Long
-        _tmpPrimitive = _cursor.getLong(_cursorIndexOfPrimitive)
+        _tmpPrimitive = _stmt.getLong(_cursorIndexOfPrimitive)
         val _tmpString: String
-        _tmpString = _cursor.getString(_cursorIndexOfString)
+        _tmpString = _stmt.getText(_cursorIndexOfString)
         val _tmpNullableString: String?
-        if (_cursor.isNull(_cursorIndexOfNullableString)) {
+        if (_stmt.isNull(_cursorIndexOfNullableString)) {
           _tmpNullableString = null
         } else {
-          _tmpNullableString = _cursor.getString(_cursorIndexOfNullableString)
+          _tmpNullableString = _stmt.getText(_cursorIndexOfNullableString)
         }
         val _tmpFieldString: String
-        _tmpFieldString = _cursor.getString(_cursorIndexOfFieldString)
+        _tmpFieldString = _stmt.getText(_cursorIndexOfFieldString)
         val _tmpNullableFieldString: String?
-        if (_cursor.isNull(_cursorIndexOfNullableFieldString)) {
+        if (_stmt.isNull(_cursorIndexOfNullableFieldString)) {
           _tmpNullableFieldString = null
         } else {
-          _tmpNullableFieldString = _cursor.getString(_cursorIndexOfNullableFieldString)
+          _tmpNullableFieldString = _stmt.getText(_cursorIndexOfNullableFieldString)
         }
         _result =
             MyEntity(_tmpPk,_tmpPrimitive,_tmpString,_tmpNullableString,_tmpFieldString,_tmpNullableFieldString)
-        _result.variablePrimitive = _cursor.getLong(_cursorIndexOfVariablePrimitive)
-        _result.variableString = _cursor.getString(_cursorIndexOfVariableString)
-        if (_cursor.isNull(_cursorIndexOfVariableNullableString)) {
+        _result.variablePrimitive = _stmt.getLong(_cursorIndexOfVariablePrimitive)
+        _result.variableString = _stmt.getText(_cursorIndexOfVariableString)
+        if (_stmt.isNull(_cursorIndexOfVariableNullableString)) {
           _result.variableNullableString = null
         } else {
-          _result.variableNullableString = _cursor.getString(_cursorIndexOfVariableNullableString)
+          _result.variableNullableString = _stmt.getText(_cursorIndexOfVariableNullableString)
         }
-        _result.variableFieldString = _cursor.getString(_cursorIndexOfVariableFieldString)
-        if (_cursor.isNull(_cursorIndexOfVariableNullableFieldString)) {
+        _result.variableFieldString = _stmt.getText(_cursorIndexOfVariableFieldString)
+        if (_stmt.isNull(_cursorIndexOfVariableNullableFieldString)) {
           _result.variableNullableFieldString = null
         } else {
           _result.variableNullableFieldString =
-              _cursor.getString(_cursorIndexOfVariableNullableFieldString)
+              _stmt.getText(_cursorIndexOfVariableNullableFieldString)
         }
       } else {
         error("The query result was empty, but expected a single row to return a NON-NULL object of type <MyEntity>.")
       }
-      return _result
-    } finally {
-      _cursor.close()
-      _statement.release()
+      _result
     }
   }
 
diff --git a/room/room-compiler/src/test/test-data/kotlinCodeGen/pojoRowAdapter_primitives.kt b/room/room-compiler/src/test/test-data/kotlinCodeGen/pojoRowAdapter_primitives.kt
index c68ee4e..d965ec0 100644
--- a/room/room-compiler/src/test/test-data/kotlinCodeGen/pojoRowAdapter_primitives.kt
+++ b/room/room-compiler/src/test/test-data/kotlinCodeGen/pojoRowAdapter_primitives.kt
@@ -1,10 +1,7 @@
-import android.database.Cursor
 import androidx.room.EntityInsertionAdapter
 import androidx.room.RoomDatabase
-import androidx.room.RoomSQLiteQuery
-import androidx.room.RoomSQLiteQuery.Companion.acquire
 import androidx.room.util.getColumnIndexOrThrow
-import androidx.room.util.query
+import androidx.room.util.performReadBlocking
 import androidx.sqlite.db.SupportSQLiteStatement
 import javax.`annotation`.processing.Generated
 import kotlin.Byte
@@ -59,41 +56,35 @@
 
   public override fun getEntity(): MyEntity {
     val _sql: String = "SELECT * FROM MyEntity"
-    val _statement: RoomSQLiteQuery = acquire(_sql, 0)
-    __db.assertNotSuspendingTransaction()
-    val _cursor: Cursor = query(__db, _statement, false, null)
-    try {
-      val _cursorIndexOfInt: Int = getColumnIndexOrThrow(_cursor, "int")
-      val _cursorIndexOfShort: Int = getColumnIndexOrThrow(_cursor, "short")
-      val _cursorIndexOfByte: Int = getColumnIndexOrThrow(_cursor, "byte")
-      val _cursorIndexOfLong: Int = getColumnIndexOrThrow(_cursor, "long")
-      val _cursorIndexOfChar: Int = getColumnIndexOrThrow(_cursor, "char")
-      val _cursorIndexOfFloat: Int = getColumnIndexOrThrow(_cursor, "float")
-      val _cursorIndexOfDouble: Int = getColumnIndexOrThrow(_cursor, "double")
+    return performReadBlocking(__db, _sql) { _stmt ->
+      val _cursorIndexOfInt: Int = getColumnIndexOrThrow(_stmt, "int")
+      val _cursorIndexOfShort: Int = getColumnIndexOrThrow(_stmt, "short")
+      val _cursorIndexOfByte: Int = getColumnIndexOrThrow(_stmt, "byte")
+      val _cursorIndexOfLong: Int = getColumnIndexOrThrow(_stmt, "long")
+      val _cursorIndexOfChar: Int = getColumnIndexOrThrow(_stmt, "char")
+      val _cursorIndexOfFloat: Int = getColumnIndexOrThrow(_stmt, "float")
+      val _cursorIndexOfDouble: Int = getColumnIndexOrThrow(_stmt, "double")
       val _result: MyEntity
-      if (_cursor.moveToFirst()) {
+      if (_stmt.step()) {
         val _tmpInt: Int
-        _tmpInt = _cursor.getInt(_cursorIndexOfInt)
+        _tmpInt = _stmt.getLong(_cursorIndexOfInt).toInt()
         val _tmpShort: Short
-        _tmpShort = _cursor.getShort(_cursorIndexOfShort)
+        _tmpShort = _stmt.getLong(_cursorIndexOfShort).toShort()
         val _tmpByte: Byte
-        _tmpByte = _cursor.getShort(_cursorIndexOfByte).toByte()
+        _tmpByte = _stmt.getLong(_cursorIndexOfByte).toByte()
         val _tmpLong: Long
-        _tmpLong = _cursor.getLong(_cursorIndexOfLong)
+        _tmpLong = _stmt.getLong(_cursorIndexOfLong)
         val _tmpChar: Char
-        _tmpChar = _cursor.getInt(_cursorIndexOfChar).toChar()
+        _tmpChar = _stmt.getLong(_cursorIndexOfChar).toChar()
         val _tmpFloat: Float
-        _tmpFloat = _cursor.getFloat(_cursorIndexOfFloat)
+        _tmpFloat = _stmt.getDouble(_cursorIndexOfFloat).toFloat()
         val _tmpDouble: Double
-        _tmpDouble = _cursor.getDouble(_cursorIndexOfDouble)
+        _tmpDouble = _stmt.getDouble(_cursorIndexOfDouble)
         _result = MyEntity(_tmpInt,_tmpShort,_tmpByte,_tmpLong,_tmpChar,_tmpFloat,_tmpDouble)
       } else {
         error("The query result was empty, but expected a single row to return a NON-NULL object of type <MyEntity>.")
       }
-      return _result
-    } finally {
-      _cursor.close()
-      _statement.release()
+      _result
     }
   }
 
diff --git a/room/room-compiler/src/test/test-data/kotlinCodeGen/pojoRowAdapter_primitives_nullable.kt b/room/room-compiler/src/test/test-data/kotlinCodeGen/pojoRowAdapter_primitives_nullable.kt
index d30d498..a870118 100644
--- a/room/room-compiler/src/test/test-data/kotlinCodeGen/pojoRowAdapter_primitives_nullable.kt
+++ b/room/room-compiler/src/test/test-data/kotlinCodeGen/pojoRowAdapter_primitives_nullable.kt
@@ -1,10 +1,7 @@
-import android.database.Cursor
 import androidx.room.EntityInsertionAdapter
 import androidx.room.RoomDatabase
-import androidx.room.RoomSQLiteQuery
-import androidx.room.RoomSQLiteQuery.Companion.acquire
 import androidx.room.util.getColumnIndexOrThrow
-import androidx.room.util.query
+import androidx.room.util.performReadBlocking
 import androidx.sqlite.db.SupportSQLiteStatement
 import javax.`annotation`.processing.Generated
 import kotlin.Byte
@@ -94,69 +91,63 @@
 
   public override fun getEntity(): MyEntity {
     val _sql: String = "SELECT * FROM MyEntity"
-    val _statement: RoomSQLiteQuery = acquire(_sql, 0)
-    __db.assertNotSuspendingTransaction()
-    val _cursor: Cursor = query(__db, _statement, false, null)
-    try {
-      val _cursorIndexOfInt: Int = getColumnIndexOrThrow(_cursor, "int")
-      val _cursorIndexOfShort: Int = getColumnIndexOrThrow(_cursor, "short")
-      val _cursorIndexOfByte: Int = getColumnIndexOrThrow(_cursor, "byte")
-      val _cursorIndexOfLong: Int = getColumnIndexOrThrow(_cursor, "long")
-      val _cursorIndexOfChar: Int = getColumnIndexOrThrow(_cursor, "char")
-      val _cursorIndexOfFloat: Int = getColumnIndexOrThrow(_cursor, "float")
-      val _cursorIndexOfDouble: Int = getColumnIndexOrThrow(_cursor, "double")
+    return performReadBlocking(__db, _sql) { _stmt ->
+      val _cursorIndexOfInt: Int = getColumnIndexOrThrow(_stmt, "int")
+      val _cursorIndexOfShort: Int = getColumnIndexOrThrow(_stmt, "short")
+      val _cursorIndexOfByte: Int = getColumnIndexOrThrow(_stmt, "byte")
+      val _cursorIndexOfLong: Int = getColumnIndexOrThrow(_stmt, "long")
+      val _cursorIndexOfChar: Int = getColumnIndexOrThrow(_stmt, "char")
+      val _cursorIndexOfFloat: Int = getColumnIndexOrThrow(_stmt, "float")
+      val _cursorIndexOfDouble: Int = getColumnIndexOrThrow(_stmt, "double")
       val _result: MyEntity
-      if (_cursor.moveToFirst()) {
+      if (_stmt.step()) {
         val _tmpInt: Int?
-        if (_cursor.isNull(_cursorIndexOfInt)) {
+        if (_stmt.isNull(_cursorIndexOfInt)) {
           _tmpInt = null
         } else {
-          _tmpInt = _cursor.getInt(_cursorIndexOfInt)
+          _tmpInt = _stmt.getLong(_cursorIndexOfInt).toInt()
         }
         val _tmpShort: Short?
-        if (_cursor.isNull(_cursorIndexOfShort)) {
+        if (_stmt.isNull(_cursorIndexOfShort)) {
           _tmpShort = null
         } else {
-          _tmpShort = _cursor.getShort(_cursorIndexOfShort)
+          _tmpShort = _stmt.getLong(_cursorIndexOfShort).toShort()
         }
         val _tmpByte: Byte?
-        if (_cursor.isNull(_cursorIndexOfByte)) {
+        if (_stmt.isNull(_cursorIndexOfByte)) {
           _tmpByte = null
         } else {
-          _tmpByte = _cursor.getShort(_cursorIndexOfByte).toByte()
+          _tmpByte = _stmt.getLong(_cursorIndexOfByte).toByte()
         }
         val _tmpLong: Long?
-        if (_cursor.isNull(_cursorIndexOfLong)) {
+        if (_stmt.isNull(_cursorIndexOfLong)) {
           _tmpLong = null
         } else {
-          _tmpLong = _cursor.getLong(_cursorIndexOfLong)
+          _tmpLong = _stmt.getLong(_cursorIndexOfLong)
         }
         val _tmpChar: Char?
-        if (_cursor.isNull(_cursorIndexOfChar)) {
+        if (_stmt.isNull(_cursorIndexOfChar)) {
           _tmpChar = null
         } else {
-          _tmpChar = _cursor.getInt(_cursorIndexOfChar).toChar()
+          _tmpChar = _stmt.getLong(_cursorIndexOfChar).toChar()
         }
         val _tmpFloat: Float?
-        if (_cursor.isNull(_cursorIndexOfFloat)) {
+        if (_stmt.isNull(_cursorIndexOfFloat)) {
           _tmpFloat = null
         } else {
-          _tmpFloat = _cursor.getFloat(_cursorIndexOfFloat)
+          _tmpFloat = _stmt.getDouble(_cursorIndexOfFloat).toFloat()
         }
         val _tmpDouble: Double?
-        if (_cursor.isNull(_cursorIndexOfDouble)) {
+        if (_stmt.isNull(_cursorIndexOfDouble)) {
           _tmpDouble = null
         } else {
-          _tmpDouble = _cursor.getDouble(_cursorIndexOfDouble)
+          _tmpDouble = _stmt.getDouble(_cursorIndexOfDouble)
         }
         _result = MyEntity(_tmpInt,_tmpShort,_tmpByte,_tmpLong,_tmpChar,_tmpFloat,_tmpDouble)
       } else {
         error("The query result was empty, but expected a single row to return a NON-NULL object of type <MyEntity>.")
       }
-      return _result
-    } finally {
-      _cursor.close()
-      _statement.release()
+      _result
     }
   }
 
diff --git a/room/room-compiler/src/test/test-data/kotlinCodeGen/pojoRowAdapter_string.kt b/room/room-compiler/src/test/test-data/kotlinCodeGen/pojoRowAdapter_string.kt
index 6a4aa8b..848194c 100644
--- a/room/room-compiler/src/test/test-data/kotlinCodeGen/pojoRowAdapter_string.kt
+++ b/room/room-compiler/src/test/test-data/kotlinCodeGen/pojoRowAdapter_string.kt
@@ -1,10 +1,7 @@
-import android.database.Cursor
 import androidx.room.EntityInsertionAdapter
 import androidx.room.RoomDatabase
-import androidx.room.RoomSQLiteQuery
-import androidx.room.RoomSQLiteQuery.Companion.acquire
 import androidx.room.util.getColumnIndexOrThrow
-import androidx.room.util.query
+import androidx.room.util.performReadBlocking
 import androidx.sqlite.db.SupportSQLiteStatement
 import javax.`annotation`.processing.Generated
 import kotlin.Int
@@ -53,30 +50,24 @@
 
   public override fun getEntity(): MyEntity {
     val _sql: String = "SELECT * FROM MyEntity"
-    val _statement: RoomSQLiteQuery = acquire(_sql, 0)
-    __db.assertNotSuspendingTransaction()
-    val _cursor: Cursor = query(__db, _statement, false, null)
-    try {
-      val _cursorIndexOfString: Int = getColumnIndexOrThrow(_cursor, "string")
-      val _cursorIndexOfNullableString: Int = getColumnIndexOrThrow(_cursor, "nullableString")
+    return performReadBlocking(__db, _sql) { _stmt ->
+      val _cursorIndexOfString: Int = getColumnIndexOrThrow(_stmt, "string")
+      val _cursorIndexOfNullableString: Int = getColumnIndexOrThrow(_stmt, "nullableString")
       val _result: MyEntity
-      if (_cursor.moveToFirst()) {
+      if (_stmt.step()) {
         val _tmpString: String
-        _tmpString = _cursor.getString(_cursorIndexOfString)
+        _tmpString = _stmt.getText(_cursorIndexOfString)
         val _tmpNullableString: String?
-        if (_cursor.isNull(_cursorIndexOfNullableString)) {
+        if (_stmt.isNull(_cursorIndexOfNullableString)) {
           _tmpNullableString = null
         } else {
-          _tmpNullableString = _cursor.getString(_cursorIndexOfNullableString)
+          _tmpNullableString = _stmt.getText(_cursorIndexOfNullableString)
         }
         _result = MyEntity(_tmpString,_tmpNullableString)
       } else {
         error("The query result was empty, but expected a single row to return a NON-NULL object of type <MyEntity>.")
       }
-      return _result
-    } finally {
-      _cursor.close()
-      _statement.release()
+      _result
     }
   }
 
diff --git a/room/room-compiler/src/test/test-data/kotlinCodeGen/pojoRowAdapter_uuid.kt b/room/room-compiler/src/test/test-data/kotlinCodeGen/pojoRowAdapter_uuid.kt
index 04e8b45..5f0966c 100644
--- a/room/room-compiler/src/test/test-data/kotlinCodeGen/pojoRowAdapter_uuid.kt
+++ b/room/room-compiler/src/test/test-data/kotlinCodeGen/pojoRowAdapter_uuid.kt
@@ -1,12 +1,9 @@
-import android.database.Cursor
 import androidx.room.EntityInsertionAdapter
 import androidx.room.RoomDatabase
-import androidx.room.RoomSQLiteQuery
-import androidx.room.RoomSQLiteQuery.Companion.acquire
 import androidx.room.util.convertByteToUUID
 import androidx.room.util.convertUUIDToByte
 import androidx.room.util.getColumnIndexOrThrow
-import androidx.room.util.query
+import androidx.room.util.performReadBlocking
 import androidx.sqlite.db.SupportSQLiteStatement
 import java.util.UUID
 import javax.`annotation`.processing.Generated
@@ -57,33 +54,27 @@
 
   public override fun getEntity(): MyEntity {
     val _sql: String = "SELECT * FROM MyEntity"
-    val _statement: RoomSQLiteQuery = acquire(_sql, 0)
-    __db.assertNotSuspendingTransaction()
-    val _cursor: Cursor = query(__db, _statement, false, null)
-    try {
-      val _cursorIndexOfPk: Int = getColumnIndexOrThrow(_cursor, "pk")
-      val _cursorIndexOfUuid: Int = getColumnIndexOrThrow(_cursor, "uuid")
-      val _cursorIndexOfNullableUuid: Int = getColumnIndexOrThrow(_cursor, "nullableUuid")
+    return performReadBlocking(__db, _sql) { _stmt ->
+      val _cursorIndexOfPk: Int = getColumnIndexOrThrow(_stmt, "pk")
+      val _cursorIndexOfUuid: Int = getColumnIndexOrThrow(_stmt, "uuid")
+      val _cursorIndexOfNullableUuid: Int = getColumnIndexOrThrow(_stmt, "nullableUuid")
       val _result: MyEntity
-      if (_cursor.moveToFirst()) {
+      if (_stmt.step()) {
         val _tmpPk: Int
-        _tmpPk = _cursor.getInt(_cursorIndexOfPk)
+        _tmpPk = _stmt.getLong(_cursorIndexOfPk).toInt()
         val _tmpUuid: UUID
-        _tmpUuid = convertByteToUUID(_cursor.getBlob(_cursorIndexOfUuid))
+        _tmpUuid = convertByteToUUID(_stmt.getBlob(_cursorIndexOfUuid))
         val _tmpNullableUuid: UUID?
-        if (_cursor.isNull(_cursorIndexOfNullableUuid)) {
+        if (_stmt.isNull(_cursorIndexOfNullableUuid)) {
           _tmpNullableUuid = null
         } else {
-          _tmpNullableUuid = convertByteToUUID(_cursor.getBlob(_cursorIndexOfNullableUuid))
+          _tmpNullableUuid = convertByteToUUID(_stmt.getBlob(_cursorIndexOfNullableUuid))
         }
         _result = MyEntity(_tmpPk,_tmpUuid,_tmpNullableUuid)
       } else {
         error("The query result was empty, but expected a single row to return a NON-NULL object of type <MyEntity>.")
       }
-      return _result
-    } finally {
-      _cursor.close()
-      _statement.release()
+      _result
     }
   }
 
diff --git a/room/room-compiler/src/test/test-data/kotlinCodeGen/pojoRowAdapter_valueClassConverter.kt b/room/room-compiler/src/test/test-data/kotlinCodeGen/pojoRowAdapter_valueClassConverter.kt
index 817dc88..12f7417 100644
--- a/room/room-compiler/src/test/test-data/kotlinCodeGen/pojoRowAdapter_valueClassConverter.kt
+++ b/room/room-compiler/src/test/test-data/kotlinCodeGen/pojoRowAdapter_valueClassConverter.kt
@@ -1,12 +1,9 @@
-import android.database.Cursor
 import androidx.room.EntityInsertionAdapter
 import androidx.room.RoomDatabase
-import androidx.room.RoomSQLiteQuery
-import androidx.room.RoomSQLiteQuery.Companion.acquire
 import androidx.room.util.convertByteToUUID
 import androidx.room.util.convertUUIDToByte
 import androidx.room.util.getColumnIndexOrThrow
-import androidx.room.util.query
+import androidx.room.util.performReadBlocking
 import androidx.sqlite.db.SupportSQLiteStatement
 import java.util.UUID
 import javax.`annotation`.processing.Generated
@@ -80,60 +77,54 @@
 
   public override fun getEntity(): MyEntity {
     val _sql: String = "SELECT * FROM MyEntity"
-    val _statement: RoomSQLiteQuery = acquire(_sql, 0)
-    __db.assertNotSuspendingTransaction()
-    val _cursor: Cursor = query(__db, _statement, false, null)
-    try {
-      val _cursorIndexOfPk: Int = getColumnIndexOrThrow(_cursor, "pk")
-      val _cursorIndexOfUuidData: Int = getColumnIndexOrThrow(_cursor, "uuidData")
-      val _cursorIndexOfNullableUuidData: Int = getColumnIndexOrThrow(_cursor, "nullableUuidData")
-      val _cursorIndexOfNullableLongData: Int = getColumnIndexOrThrow(_cursor, "nullableLongData")
-      val _cursorIndexOfDoubleNullableLongData: Int = getColumnIndexOrThrow(_cursor,
+    return performReadBlocking(__db, _sql) { _stmt ->
+      val _cursorIndexOfPk: Int = getColumnIndexOrThrow(_stmt, "pk")
+      val _cursorIndexOfUuidData: Int = getColumnIndexOrThrow(_stmt, "uuidData")
+      val _cursorIndexOfNullableUuidData: Int = getColumnIndexOrThrow(_stmt, "nullableUuidData")
+      val _cursorIndexOfNullableLongData: Int = getColumnIndexOrThrow(_stmt, "nullableLongData")
+      val _cursorIndexOfDoubleNullableLongData: Int = getColumnIndexOrThrow(_stmt,
           "doubleNullableLongData")
-      val _cursorIndexOfGenericData: Int = getColumnIndexOrThrow(_cursor, "genericData")
+      val _cursorIndexOfGenericData: Int = getColumnIndexOrThrow(_stmt, "genericData")
       val _result: MyEntity
-      if (_cursor.moveToFirst()) {
+      if (_stmt.step()) {
         val _tmpPk: LongValueClass
         val _data: Long
-        _data = _cursor.getLong(_cursorIndexOfPk)
+        _data = _stmt.getLong(_cursorIndexOfPk)
         _tmpPk = LongValueClass(_data)
         val _tmpUuidData: UUIDValueClass
         val _data_1: UUID
-        _data_1 = convertByteToUUID(_cursor.getBlob(_cursorIndexOfUuidData))
+        _data_1 = convertByteToUUID(_stmt.getBlob(_cursorIndexOfUuidData))
         _tmpUuidData = UUIDValueClass(_data_1)
         val _tmpNullableUuidData: UUIDValueClass?
-        if (_cursor.isNull(_cursorIndexOfNullableUuidData)) {
+        if (_stmt.isNull(_cursorIndexOfNullableUuidData)) {
           _tmpNullableUuidData = null
         } else {
           val _data_2: UUID
-          _data_2 = convertByteToUUID(_cursor.getBlob(_cursorIndexOfNullableUuidData))
+          _data_2 = convertByteToUUID(_stmt.getBlob(_cursorIndexOfNullableUuidData))
           _tmpNullableUuidData = UUIDValueClass(_data_2)
         }
         val _tmpNullableLongData: NullableLongValueClass
         val _data_3: Long
-        _data_3 = _cursor.getLong(_cursorIndexOfNullableLongData)
+        _data_3 = _stmt.getLong(_cursorIndexOfNullableLongData)
         _tmpNullableLongData = NullableLongValueClass(_data_3)
         val _tmpDoubleNullableLongData: NullableLongValueClass?
-        if (_cursor.isNull(_cursorIndexOfDoubleNullableLongData)) {
+        if (_stmt.isNull(_cursorIndexOfDoubleNullableLongData)) {
           _tmpDoubleNullableLongData = null
         } else {
           val _data_4: Long
-          _data_4 = _cursor.getLong(_cursorIndexOfDoubleNullableLongData)
+          _data_4 = _stmt.getLong(_cursorIndexOfDoubleNullableLongData)
           _tmpDoubleNullableLongData = NullableLongValueClass(_data_4)
         }
         val _tmpGenericData: GenericValueClass<String>
         val _password: String
-        _password = _cursor.getString(_cursorIndexOfGenericData)
+        _password = _stmt.getText(_cursorIndexOfGenericData)
         _tmpGenericData = GenericValueClass<String>(_password)
         _result =
             MyEntity(_tmpPk,_tmpUuidData,_tmpNullableUuidData,_tmpNullableLongData,_tmpDoubleNullableLongData,_tmpGenericData)
       } else {
         error("The query result was empty, but expected a single row to return a NON-NULL object of type <MyEntity>.")
       }
-      return _result
-    } finally {
-      _cursor.close()
-      _statement.release()
+      _result
     }
   }
 
diff --git a/room/room-compiler/src/test/test-data/kotlinCodeGen/pojoRowAdapter_variableProperty.kt b/room/room-compiler/src/test/test-data/kotlinCodeGen/pojoRowAdapter_variableProperty.kt
index b625f23..dc8ddc9 100644
--- a/room/room-compiler/src/test/test-data/kotlinCodeGen/pojoRowAdapter_variableProperty.kt
+++ b/room/room-compiler/src/test/test-data/kotlinCodeGen/pojoRowAdapter_variableProperty.kt
@@ -1,10 +1,7 @@
-import android.database.Cursor
 import androidx.room.EntityInsertionAdapter
 import androidx.room.RoomDatabase
-import androidx.room.RoomSQLiteQuery
-import androidx.room.RoomSQLiteQuery.Companion.acquire
 import androidx.room.util.getColumnIndexOrThrow
-import androidx.room.util.query
+import androidx.room.util.performReadBlocking
 import androidx.sqlite.db.SupportSQLiteStatement
 import javax.`annotation`.processing.Generated
 import kotlin.Int
@@ -55,34 +52,28 @@
 
   public override fun getEntity(): MyEntity {
     val _sql: String = "SELECT * FROM MyEntity"
-    val _statement: RoomSQLiteQuery = acquire(_sql, 0)
-    __db.assertNotSuspendingTransaction()
-    val _cursor: Cursor = query(__db, _statement, false, null)
-    try {
-      val _cursorIndexOfPk: Int = getColumnIndexOrThrow(_cursor, "pk")
-      val _cursorIndexOfVariablePrimitive: Int = getColumnIndexOrThrow(_cursor, "variablePrimitive")
-      val _cursorIndexOfVariableString: Int = getColumnIndexOrThrow(_cursor, "variableString")
-      val _cursorIndexOfVariableNullableString: Int = getColumnIndexOrThrow(_cursor,
+    return performReadBlocking(__db, _sql) { _stmt ->
+      val _cursorIndexOfPk: Int = getColumnIndexOrThrow(_stmt, "pk")
+      val _cursorIndexOfVariablePrimitive: Int = getColumnIndexOrThrow(_stmt, "variablePrimitive")
+      val _cursorIndexOfVariableString: Int = getColumnIndexOrThrow(_stmt, "variableString")
+      val _cursorIndexOfVariableNullableString: Int = getColumnIndexOrThrow(_stmt,
           "variableNullableString")
       val _result: MyEntity
-      if (_cursor.moveToFirst()) {
+      if (_stmt.step()) {
         val _tmpPk: Int
-        _tmpPk = _cursor.getInt(_cursorIndexOfPk)
+        _tmpPk = _stmt.getLong(_cursorIndexOfPk).toInt()
         _result = MyEntity(_tmpPk)
-        _result.variablePrimitive = _cursor.getLong(_cursorIndexOfVariablePrimitive)
-        _result.variableString = _cursor.getString(_cursorIndexOfVariableString)
-        if (_cursor.isNull(_cursorIndexOfVariableNullableString)) {
+        _result.variablePrimitive = _stmt.getLong(_cursorIndexOfVariablePrimitive)
+        _result.variableString = _stmt.getText(_cursorIndexOfVariableString)
+        if (_stmt.isNull(_cursorIndexOfVariableNullableString)) {
           _result.variableNullableString = null
         } else {
-          _result.variableNullableString = _cursor.getString(_cursorIndexOfVariableNullableString)
+          _result.variableNullableString = _stmt.getText(_cursorIndexOfVariableNullableString)
         }
       } else {
         error("The query result was empty, but expected a single row to return a NON-NULL object of type <MyEntity>.")
       }
-      return _result
-    } finally {
-      _cursor.close()
-      _statement.release()
+      _result
     }
   }
 
diff --git a/room/room-compiler/src/test/test-data/kotlinCodeGen/pojoRowAdapter_variableProperty_java.kt b/room/room-compiler/src/test/test-data/kotlinCodeGen/pojoRowAdapter_variableProperty_java.kt
index 500ed9e..c111064 100644
--- a/room/room-compiler/src/test/test-data/kotlinCodeGen/pojoRowAdapter_variableProperty_java.kt
+++ b/room/room-compiler/src/test/test-data/kotlinCodeGen/pojoRowAdapter_variableProperty_java.kt
@@ -1,10 +1,7 @@
-import android.database.Cursor
 import androidx.room.EntityInsertionAdapter
 import androidx.room.RoomDatabase
-import androidx.room.RoomSQLiteQuery
-import androidx.room.RoomSQLiteQuery.Companion.acquire
 import androidx.room.util.getColumnIndexOrThrow
-import androidx.room.util.query
+import androidx.room.util.performReadBlocking
 import androidx.sqlite.db.SupportSQLiteStatement
 import javax.`annotation`.processing.Generated
 import kotlin.Int
@@ -54,32 +51,26 @@
 
   public override fun getEntity(): MyEntity {
     val _sql: String = "SELECT * FROM MyEntity"
-    val _statement: RoomSQLiteQuery = acquire(_sql, 0)
-    __db.assertNotSuspendingTransaction()
-    val _cursor: Cursor = query(__db, _statement, false, null)
-    try {
-      val _cursorIndexOfMValue: Int = getColumnIndexOrThrow(_cursor, "mValue")
-      val _cursorIndexOfMNullableValue: Int = getColumnIndexOrThrow(_cursor, "mNullableValue")
+    return performReadBlocking(__db, _sql) { _stmt ->
+      val _cursorIndexOfMValue: Int = getColumnIndexOrThrow(_stmt, "mValue")
+      val _cursorIndexOfMNullableValue: Int = getColumnIndexOrThrow(_stmt, "mNullableValue")
       val _result: MyEntity
-      if (_cursor.moveToFirst()) {
+      if (_stmt.step()) {
         _result = MyEntity()
         val _tmpMValue: Long
-        _tmpMValue = _cursor.getLong(_cursorIndexOfMValue)
+        _tmpMValue = _stmt.getLong(_cursorIndexOfMValue)
         _result.setValue(_tmpMValue)
         val _tmpMNullableValue: String?
-        if (_cursor.isNull(_cursorIndexOfMNullableValue)) {
+        if (_stmt.isNull(_cursorIndexOfMNullableValue)) {
           _tmpMNullableValue = null
         } else {
-          _tmpMNullableValue = _cursor.getString(_cursorIndexOfMNullableValue)
+          _tmpMNullableValue = _stmt.getText(_cursorIndexOfMNullableValue)
         }
         _result.setNullableValue(_tmpMNullableValue)
       } else {
         error("The query result was empty, but expected a single row to return a NON-NULL object of type <MyEntity>.")
       }
-      return _result
-    } finally {
-      _cursor.close()
-      _statement.release()
+      _result
     }
   }
 
diff --git a/room/room-runtime/api/restricted_current.ignore b/room/room-runtime/api/restricted_current.ignore
new file mode 100644
index 0000000..c105ab0
--- /dev/null
+++ b/room/room-runtime/api/restricted_current.ignore
@@ -0,0 +1,9 @@
+// Baseline format: 1.0
+DefaultValueChange: androidx.room.util.TableInfo#TableInfo(String, java.util.Map<java.lang.String,androidx.room.util.TableInfo.Column>, java.util.Set<androidx.room.util.TableInfo.ForeignKey>, java.util.Set<androidx.room.util.TableInfo.Index>) parameter #3:
+    Attempted to remove default value from parameter indices in androidx.room.util.TableInfo
+
+
+RemovedMethod: androidx.room.util.FtsTableInfo#parseOptions(String):
+    Removed method androidx.room.util.FtsTableInfo.parseOptions(String)
+RemovedMethod: androidx.room.util.FtsTableInfo.Companion#parseOptions(String):
+    Removed method androidx.room.util.FtsTableInfo.Companion.parseOptions(String)
diff --git a/room/room-runtime/api/restricted_current.txt b/room/room-runtime/api/restricted_current.txt
index ae0bc5e..54202fc 100644
--- a/room/room-runtime/api/restricted_current.txt
+++ b/room/room-runtime/api/restricted_current.txt
@@ -150,6 +150,7 @@
     method public boolean inTransaction();
     method @CallSuper public void init(androidx.room.DatabaseConfiguration configuration);
     method @Deprecated protected void internalInitInvalidationTracker(androidx.sqlite.db.SupportSQLiteDatabase db);
+    method @RestrictTo(androidx.annotation.RestrictTo.Scope.LIBRARY_GROUP_PREFIX) protected final void internalInitInvalidationTracker(androidx.sqlite.SQLiteConnection connection);
     method public boolean isOpen();
     method public android.database.Cursor query(androidx.sqlite.db.SupportSQLiteQuery query);
     method public android.database.Cursor query(androidx.sqlite.db.SupportSQLiteQuery query, optional android.os.CancellationSignal? signal);
@@ -398,6 +399,8 @@
     method @Deprecated @RestrictTo(androidx.annotation.RestrictTo.Scope.LIBRARY_GROUP_PREFIX) public static void dropFtsSyncTriggers(androidx.sqlite.db.SupportSQLiteDatabase db);
     method @RestrictTo(androidx.annotation.RestrictTo.Scope.LIBRARY_GROUP_PREFIX) public static void dropFtsSyncTriggers(androidx.sqlite.SQLiteConnection connection);
     method @RestrictTo(androidx.annotation.RestrictTo.Scope.LIBRARY_GROUP_PREFIX) public static void foreignKeyCheck(androidx.sqlite.db.SupportSQLiteDatabase db, String tableName);
+    method @RestrictTo(androidx.annotation.RestrictTo.Scope.LIBRARY_GROUP_PREFIX) public static <R> R performReadBlocking(androidx.room.RoomDatabase db, String sql, kotlin.jvm.functions.Function1<? super androidx.sqlite.SQLiteStatement,? extends R> block);
+    method @RestrictTo(androidx.annotation.RestrictTo.Scope.LIBRARY_GROUP_PREFIX) public static <R> R performReadTransactionBlocking(androidx.room.RoomDatabase db, String sql, kotlin.jvm.functions.Function1<? super androidx.sqlite.SQLiteStatement,? extends R> block);
     method @Deprecated @RestrictTo(androidx.annotation.RestrictTo.Scope.LIBRARY_GROUP_PREFIX) public static android.database.Cursor query(androidx.room.RoomDatabase db, androidx.sqlite.db.SupportSQLiteQuery sqLiteQuery, boolean maybeCopy);
     method @RestrictTo(androidx.annotation.RestrictTo.Scope.LIBRARY_GROUP_PREFIX) public static android.database.Cursor query(androidx.room.RoomDatabase db, androidx.sqlite.db.SupportSQLiteQuery sqLiteQuery, boolean maybeCopy, android.os.CancellationSignal? signal);
     method @RestrictTo(androidx.annotation.RestrictTo.Scope.LIBRARY_GROUP_PREFIX) @kotlin.jvm.Throws(exceptionClasses=IOException::class) public static int readVersion(java.io.File databaseFile) throws java.io.IOException;
@@ -410,7 +413,6 @@
   @RestrictTo(androidx.annotation.RestrictTo.Scope.LIBRARY_GROUP_PREFIX) public final class FtsTableInfo {
     ctor public FtsTableInfo(String name, java.util.Set<java.lang.String> columns, String createSql);
     ctor public FtsTableInfo(String name, java.util.Set<java.lang.String> columns, java.util.Set<java.lang.String> options);
-    method @VisibleForTesting public static java.util.Set<java.lang.String> parseOptions(String createStatement);
     method public static androidx.room.util.FtsTableInfo read(androidx.sqlite.db.SupportSQLiteDatabase database, String tableName);
     method public static androidx.room.util.FtsTableInfo read(androidx.sqlite.SQLiteConnection connection, String tableName);
     field public static final androidx.room.util.FtsTableInfo.Companion Companion;
@@ -420,7 +422,6 @@
   }
 
   public static final class FtsTableInfo.Companion {
-    method @VisibleForTesting public java.util.Set<java.lang.String> parseOptions(String createStatement);
     method public androidx.room.util.FtsTableInfo read(androidx.sqlite.db.SupportSQLiteDatabase database, String tableName);
     method public androidx.room.util.FtsTableInfo read(androidx.sqlite.SQLiteConnection connection, String tableName);
   }
@@ -431,6 +432,10 @@
     method public static <V> void recursiveFetchLongSparseArray(androidx.collection.LongSparseArray<V> map, boolean isRelationCollection, kotlin.jvm.functions.Function1<? super androidx.collection.LongSparseArray<V>,kotlin.Unit> fetchBlock);
   }
 
+  public final class SQLiteStatementUtil {
+    method @RestrictTo(androidx.annotation.RestrictTo.Scope.LIBRARY_GROUP_PREFIX) public static int getColumnIndexOrThrow(androidx.sqlite.SQLiteStatement stmt, String name);
+  }
+
   @RestrictTo({androidx.annotation.RestrictTo.Scope.LIBRARY_GROUP_PREFIX}) public final class StringUtil {
     method public static void appendPlaceholders(StringBuilder builder, int count);
     method public static String? joinIntoString(java.util.List<java.lang.Integer>? input);
@@ -440,9 +445,9 @@
   }
 
   @RestrictTo(androidx.annotation.RestrictTo.Scope.LIBRARY_GROUP_PREFIX) public final class TableInfo {
-    ctor public TableInfo(String name, java.util.Map<java.lang.String,androidx.room.util.TableInfo.Column> columns, java.util.Set<androidx.room.util.TableInfo.ForeignKey> foreignKeys);
-    ctor public TableInfo(String name, java.util.Map<java.lang.String,androidx.room.util.TableInfo.Column> columns, java.util.Set<androidx.room.util.TableInfo.ForeignKey> foreignKeys, optional java.util.Set<androidx.room.util.TableInfo.Index>? indices);
-    method public static androidx.room.util.TableInfo read(androidx.sqlite.db.SupportSQLiteDatabase database, String tableName);
+    ctor @Deprecated public TableInfo(String name, java.util.Map<java.lang.String,androidx.room.util.TableInfo.Column> columns, java.util.Set<androidx.room.util.TableInfo.ForeignKey> foreignKeys);
+    ctor public TableInfo(String name, java.util.Map<java.lang.String,androidx.room.util.TableInfo.Column> columns, java.util.Set<androidx.room.util.TableInfo.ForeignKey> foreignKeys, java.util.Set<androidx.room.util.TableInfo.Index>? indices);
+    method @Deprecated public static androidx.room.util.TableInfo read(androidx.sqlite.db.SupportSQLiteDatabase database, String tableName);
     method public static androidx.room.util.TableInfo read(androidx.sqlite.SQLiteConnection connection, String tableName);
     field public static final int CREATED_FROM_DATABASE = 2; // 0x2
     field public static final int CREATED_FROM_ENTITY = 1; // 0x1
@@ -454,10 +459,10 @@
     field public final String name;
   }
 
-  public static final class TableInfo.Column {
+  @RestrictTo(androidx.annotation.RestrictTo.Scope.LIBRARY_GROUP_PREFIX) public static final class TableInfo.Column {
     ctor @Deprecated public TableInfo.Column(String name, String type, boolean notNull, int primaryKeyPosition);
     ctor public TableInfo.Column(String name, String type, boolean notNull, int primaryKeyPosition, String? defaultValue, int createdFrom);
-    method @VisibleForTesting public static boolean defaultValueEquals(String current, String? other);
+    method public static boolean defaultValueEquals(String current, String? other);
     method public boolean isPrimaryKey();
     property public final boolean isPrimaryKey;
     field public static final androidx.room.util.TableInfo.Column.Companion Companion;
@@ -471,11 +476,11 @@
   }
 
   public static final class TableInfo.Column.Companion {
-    method @VisibleForTesting public boolean defaultValueEquals(String current, String? other);
+    method public boolean defaultValueEquals(String current, String? other);
   }
 
   public static final class TableInfo.Companion {
-    method public androidx.room.util.TableInfo read(androidx.sqlite.db.SupportSQLiteDatabase database, String tableName);
+    method @Deprecated public androidx.room.util.TableInfo read(androidx.sqlite.db.SupportSQLiteDatabase database, String tableName);
     method public androidx.room.util.TableInfo read(androidx.sqlite.SQLiteConnection connection, String tableName);
   }
 
@@ -509,7 +514,7 @@
 
   @RestrictTo(androidx.annotation.RestrictTo.Scope.LIBRARY_GROUP_PREFIX) public final class ViewInfo {
     ctor public ViewInfo(String name, String? sql);
-    method public static androidx.room.util.ViewInfo read(androidx.sqlite.db.SupportSQLiteDatabase database, String viewName);
+    method @Deprecated public static androidx.room.util.ViewInfo read(androidx.sqlite.db.SupportSQLiteDatabase database, String viewName);
     method public static androidx.room.util.ViewInfo read(androidx.sqlite.SQLiteConnection connection, String viewName);
     field public static final androidx.room.util.ViewInfo.Companion Companion;
     field public final String name;
@@ -517,7 +522,7 @@
   }
 
   public static final class ViewInfo.Companion {
-    method public androidx.room.util.ViewInfo read(androidx.sqlite.db.SupportSQLiteDatabase database, String viewName);
+    method @Deprecated public androidx.room.util.ViewInfo read(androidx.sqlite.db.SupportSQLiteDatabase database, String viewName);
     method public androidx.room.util.ViewInfo read(androidx.sqlite.SQLiteConnection connection, String viewName);
   }
 
diff --git a/room/room-runtime/src/androidInstrumentedTest/kotlin/androidx/room/migration/TableInfoTest.java b/room/room-runtime/src/androidInstrumentedTest/kotlin/androidx/room/migration/TableInfoTest.java
index 439997c..1793dc7 100644
--- a/room/room-runtime/src/androidInstrumentedTest/kotlin/androidx/room/migration/TableInfoTest.java
+++ b/room/room-runtime/src/androidInstrumentedTest/kotlin/androidx/room/migration/TableInfoTest.java
@@ -16,6 +16,7 @@
 
 package androidx.room.migration;
 
+import static androidx.room.util.TableInfo.Column.defaultValueEquals;
 
 import static com.google.common.truth.Truth.assertThat;
 
@@ -458,7 +459,7 @@
         assertThat("((0) + (1 + 2))")
                 .isNotEqualTo(Objects.requireNonNull(dbInfo.columns.get("name")).defaultValue);
 
-        assertThat(TableInfo.Column.defaultValueEquals(
+        assertThat(defaultValueEquals(
                 "((0) + (1 + 2))",
                 Objects.requireNonNull(dbInfo.columns.get("name")).defaultValue)).isTrue();
     }
@@ -472,7 +473,7 @@
         assertThat("(((0) + (1 + 2)))")
                 .isNotEqualTo(Objects.requireNonNull(dbInfo.columns.get("name")).defaultValue);
 
-        assertThat(TableInfo.Column.defaultValueEquals(
+        assertThat(defaultValueEquals(
                 "(((0) + (1 + 2)))",
                 Objects.requireNonNull(dbInfo.columns.get("name")).defaultValue));
     }
@@ -486,7 +487,7 @@
         assertThat("(((3 + 5) + (2 + 1)) + (1 + 2))")
                 .isNotEqualTo(Objects.requireNonNull(dbInfo.columns.get("name")).defaultValue);
 
-        assertThat(TableInfo.Column.defaultValueEquals(
+        assertThat(defaultValueEquals(
                 "(((3 + 5) + (2 + 1)) + (1 + 2))",
                 Objects.requireNonNull(dbInfo.columns.get("name")).defaultValue));
     }
@@ -500,7 +501,7 @@
         assertThat("(    (0) + (1 + 2))")
                 .isNotEqualTo(Objects.requireNonNull(dbInfo.columns.get("name")).defaultValue);
 
-        assertThat(TableInfo.Column.defaultValueEquals(
+        assertThat(defaultValueEquals(
                 "(    (0) + (1 + 2))",
                 Objects.requireNonNull(dbInfo.columns.get("name")).defaultValue));
     }
@@ -514,7 +515,7 @@
         assertThat("((0) + (1 + 2)    )")
                 .isNotEqualTo(Objects.requireNonNull(dbInfo.columns.get("name")).defaultValue);
 
-        assertThat(TableInfo.Column.defaultValueEquals(
+        assertThat(defaultValueEquals(
                 "((0) + (1 + 2)    )",
                 Objects.requireNonNull(dbInfo.columns.get("name")).defaultValue)).isTrue();
     }
diff --git a/room/room-runtime/src/androidMain/kotlin/androidx/room/InvalidationTracker.android.kt b/room/room-runtime/src/androidMain/kotlin/androidx/room/InvalidationTracker.android.kt
index 6b18661..c1192db 100644
--- a/room/room-runtime/src/androidMain/kotlin/androidx/room/InvalidationTracker.android.kt
+++ b/room/room-runtime/src/androidMain/kotlin/androidx/room/InvalidationTracker.android.kt
@@ -26,7 +26,9 @@
 import androidx.arch.core.internal.SafeIterableMap
 import androidx.lifecycle.LiveData
 import androidx.room.Room.LOG_TAG
+import androidx.room.driver.SupportSQLiteConnection
 import androidx.room.util.useCursor
+import androidx.sqlite.SQLiteConnection
 import androidx.sqlite.db.SimpleSQLiteQuery
 import androidx.sqlite.db.SupportSQLiteDatabase
 import androidx.sqlite.db.SupportSQLiteStatement
@@ -35,8 +37,8 @@
 import java.util.concurrent.atomic.AtomicBoolean
 
 /**
- * InvalidationTracker keeps a list of tables modified by queries and notifies its callbacks about
- * these tables.
+ * The invalidation tracker keeps track of modified tables by queries and notifies its registered
+ * [Observer]s about such modifications.
  */
 // Some details on how the InvalidationTracker works:
 // * An in memory table is created with (table_id, invalidated) table_id is a hardcoded int from
@@ -49,7 +51,9 @@
 // memory table table, flipping the invalidated flag ON.
 // * When multi-instance invalidation is turned on, MultiInstanceInvalidationClient will be created.
 // It works as an Observer, and notifies other instances of table invalidation.
-open class InvalidationTracker @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP_PREFIX) constructor(
+actual open class InvalidationTracker
+@RestrictTo(RestrictTo.Scope.LIBRARY_GROUP_PREFIX)
+actual constructor(
     internal val database: RoomDatabase,
     private val shadowTablesMap: Map<String, String>,
     private val viewTables: Map<String, @JvmSuppressWildcards Set<String>>,
@@ -136,9 +140,20 @@
 
     /**
      * Internal method to initialize table tracking.
-     *
-     * You should never call this method, it is called by the generated code.
      */
+    internal actual fun internalInit(connection: SQLiteConnection) {
+        if (connection is SupportSQLiteConnection) {
+            @Suppress("DEPRECATION")
+            internalInit(connection.db)
+        } else {
+            TODO("Not yet migrated to use SQLiteDriver - b/309990302")
+        }
+    }
+
+    /**
+     * Internal method to initialize table tracking.
+     */
+    @Deprecated("No longer called by generated code")
     internal fun internalInit(database: SupportSQLiteDatabase) {
         synchronized(trackerLock) {
             if (initialized) {
diff --git a/room/room-runtime/src/androidMain/kotlin/androidx/room/RoomAndroidConnectionManager.android.kt b/room/room-runtime/src/androidMain/kotlin/androidx/room/RoomAndroidConnectionManager.android.kt
index 3261f20..da3cdc4 100644
--- a/room/room-runtime/src/androidMain/kotlin/androidx/room/RoomAndroidConnectionManager.android.kt
+++ b/room/room-runtime/src/androidMain/kotlin/androidx/room/RoomAndroidConnectionManager.android.kt
@@ -25,6 +25,7 @@
 import androidx.sqlite.SQLiteStatement
 import androidx.sqlite.db.SupportSQLiteDatabase
 import androidx.sqlite.db.SupportSQLiteOpenHelper
+import androidx.sqlite.use
 
 /**
  * An Android platform specific [RoomConnectionManager] with backwards compatibility with
@@ -243,7 +244,7 @@
         private var currentTransactionType: Transactor.SQLiteTransactionType? = null
 
         override suspend fun <R> usePrepared(sql: String, block: (SQLiteStatement) -> R): R {
-            return block.invoke(delegate.prepare(sql))
+            return delegate.prepare(sql).use { block.invoke(it) }
         }
 
         // TODO(b/318767291): Add coroutine confinement like RoomDatabase.withTransaction
@@ -268,7 +269,9 @@
                 Transactor.SQLiteTransactionType.EXCLUSIVE -> db.beginTransaction()
             }
             try {
-                return SupportTransactor<R>().block()
+                val result = SupportTransactor<R>().block()
+                db.setTransactionSuccessful()
+                return result
             } catch (rollback: RollbackException) {
                 @Suppress("UNCHECKED_CAST")
                 return rollback.result as R
diff --git a/room/room-runtime/src/androidMain/kotlin/androidx/room/RoomDatabase.android.kt b/room/room-runtime/src/androidMain/kotlin/androidx/room/RoomDatabase.android.kt
index 6ad9a08..df6336b 100644
--- a/room/room-runtime/src/androidMain/kotlin/androidx/room/RoomDatabase.android.kt
+++ b/room/room-runtime/src/androidMain/kotlin/androidx/room/RoomDatabase.android.kt
@@ -22,6 +22,7 @@
 import android.content.Context
 import android.content.Intent
 import android.database.Cursor
+import android.os.Build
 import android.os.CancellationSignal
 import android.os.Looper
 import android.util.Log
@@ -38,6 +39,7 @@
 import androidx.room.util.findMigrationPath as findMigrationPathExt
 import androidx.sqlite.SQLiteConnection
 import androidx.sqlite.SQLiteDriver
+import androidx.sqlite.SQLiteStatement
 import androidx.sqlite.db.SimpleSQLiteQuery
 import androidx.sqlite.db.SupportSQLiteDatabase
 import androidx.sqlite.db.SupportSQLiteOpenHelper
@@ -129,7 +131,8 @@
      *
      * @return The invalidation tracker for the database.
      */
-    open val invalidationTracker: InvalidationTracker = createInvalidationTracker()
+    actual open val invalidationTracker: InvalidationTracker = createInvalidationTracker()
+
     private var allowMainThreadQueries = false
 
     @JvmField
@@ -362,13 +365,14 @@
     }
 
     /**
-     * Called when the RoomDatabase is created.
+     * Creates the invalidation tracker
      *
-     * This is already implemented by the generated code.
+     * An implementation of this function is generated by the Room processor. Note that this method
+     * is called when the [RoomDatabase] is initialized.
      *
-     * @return Creates a new InvalidationTracker.
+     * @return A new invalidation tracker.
      */
-    protected abstract fun createInvalidationTracker(): InvalidationTracker
+    protected actual abstract fun createInvalidationTracker(): InvalidationTracker
 
     /**
      * Returns a Map of String -> List&lt;Class&gt; where each entry has the `key` as the DAO name
@@ -446,6 +450,46 @@
     abstract fun clearAllTables()
 
     /**
+     * Performs a 'clear all tables' operation.
+     *
+     * This should only be invoked from generated code.
+     *
+     * @see [RoomDatabase.clearAllTables]
+     */
+    @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
+    protected fun performClear(hasForeignKeys: Boolean, vararg tableNames: String) {
+        assertNotMainThread()
+        assertNotSuspendingTransaction()
+        runBlocking {
+            connectionManager.useConnection(isReadOnly = false) { connection ->
+                val supportsDeferForeignKeys =
+                    Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP
+                if (hasForeignKeys && !supportsDeferForeignKeys) {
+                    connection.execSQL("PRAGMA foreign_keys = FALSE")
+                }
+                // TODO(b/309990302): Commonize Invalidation Tracker
+                invalidationTracker.syncTriggers(openHelper.writableDatabase)
+                connection.withTransaction(Transactor.SQLiteTransactionType.IMMEDIATE) {
+                    if (hasForeignKeys && supportsDeferForeignKeys) {
+                        execSQL("PRAGMA defer_foreign_keys = TRUE")
+                    }
+                    tableNames.forEach { tableName ->
+                        execSQL("DELETE FROM `$tableName`")
+                    }
+                }
+                if (hasForeignKeys && !supportsDeferForeignKeys) {
+                    connection.execSQL("PRAGMA foreign_keys = TRUE")
+                }
+                if (!connection.inTransaction()) {
+                    connection.execSQL("PRAGMA wal_checkpoint(FULL)")
+                    connection.execSQL("VACUUM")
+                    invalidationTracker.refreshVersionsAsync()
+                }
+            }
+        }
+    }
+
+    /**
      * True if database connection is open and initialized.
      *
      * When Room is configured with [RoomDatabase.Builder.setAutoCloseTimeout] the database
@@ -512,6 +556,46 @@
         }
     }
 
+    /**
+     * Performs a database operation.
+     */
+    @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
+    actual suspend fun <R> perform(
+        isReadOnly: Boolean,
+        sql: String,
+        block: (SQLiteStatement) -> R
+    ): R {
+        return connectionManager.useConnection(isReadOnly) { connection ->
+            connection.usePrepared(sql, block)
+        }
+    }
+
+    /**
+     * Performs a transactional database operation.
+     */
+    @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
+    actual suspend fun <R> performTransaction(
+        isReadOnly: Boolean,
+        block: suspend (TransactionScope<R>) -> R
+    ): R {
+        return connectionManager.useConnection(isReadOnly) { transactor ->
+            val type = if (isReadOnly) {
+                Transactor.SQLiteTransactionType.DEFERRED
+            } else {
+                Transactor.SQLiteTransactionType.IMMEDIATE
+            }
+            // TODO(b/309990302): Commonize Invalidation Tracker
+            if (!isReadOnly) {
+                invalidationTracker.syncTriggers(openHelper.writableDatabase)
+            }
+            val result = transactor.withTransaction(type, block)
+            if (!isReadOnly && !transactor.inTransaction()) {
+                invalidationTracker.refreshVersionsAsync()
+            }
+            result
+        }
+    }
+
     // Below, there are wrapper methods for SupportSQLiteDatabase. This helps us track which
     // methods we are using and also helps unit tests to mock this class without mocking
     // all SQLite database methods.
@@ -669,32 +753,25 @@
     }
 
     /**
-     * Called by the generated code when database is open.
-     *
-     * You should never call this method manually.
+     * Initialize invalidation tracker. Note that this method is called when the [RoomDatabase] is
+     * initialized and opens a database connection.
      *
      * @param db The database instance.
      */
     @Deprecated("No longer called by generated")
     protected open fun internalInitInvalidationTracker(db: SupportSQLiteDatabase) {
-        invalidationTracker.internalInit(db)
+        internalInitInvalidationTracker(SupportSQLiteConnection(db))
     }
 
     /**
-     * Called by the generated code when database is open.
-     *
-     * You should never call this method manually.
+     * Initialize invalidation tracker. Note that this method is called when the [RoomDatabase] is
+     * initialized and opens a database connection.
      *
      * @param connection The database connection.
      */
-    @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
-    protected open fun internalInitInvalidationTracker(connection: SQLiteConnection) {
-        if (connection is SupportSQLiteConnection) {
-            @Suppress("DEPRECATION")
-            internalInitInvalidationTracker(connection.db)
-        } else {
-            TODO("Not yet migrated to use SQLiteDriver")
-        }
+    @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP_PREFIX)
+    protected actual fun internalInitInvalidationTracker(connection: SQLiteConnection) {
+        invalidationTracker.internalInit(connection)
     }
 
     /**
diff --git a/room/room-runtime/src/androidMain/kotlin/androidx/room/driver/SupportSQLiteStatement.android.kt b/room/room-runtime/src/androidMain/kotlin/androidx/room/driver/SupportSQLiteStatement.android.kt
index 412b983..b3ec4a1 100644
--- a/room/room-runtime/src/androidMain/kotlin/androidx/room/driver/SupportSQLiteStatement.android.kt
+++ b/room/room-runtime/src/androidMain/kotlin/androidx/room/driver/SupportSQLiteStatement.android.kt
@@ -137,47 +137,22 @@
 
         override fun getColumnCount(): Int {
             throwIfClosed()
+            ensureCursor()
             return cursor?.columnCount ?: 0
         }
 
         override fun getColumnName(index: Int): String {
             throwIfClosed()
-            val c = throwIfNoRow()
+            ensureCursor()
+            val c = checkNotNull(cursor)
             throwIfInvalidColumn(c, index)
             return c.getColumnName(index)
         }
 
         override fun step(): Boolean {
             throwIfClosed()
-            if (cursor == null) {
-                cursor = db.query(
-                    object : SupportSQLiteQuery {
-                        override val sql: String
-                            get() = this@SupportAndroidSQLiteStatement.sql
-
-                        override fun bindTo(statement: SupportSQLiteProgram) {
-                            for (index in 1 until bindingTypes.size) {
-                                when (bindingTypes[index]) {
-                                    COLUMN_TYPE_LONG ->
-                                        statement.bindLong(index, longBindings[index])
-                                    COLUMN_TYPE_DOUBLE ->
-                                        statement.bindDouble(index, doubleBindings[index])
-                                    COLUMN_TYPE_STRING ->
-                                        statement.bindString(index, stringBindings[index]!!)
-                                    COLUMN_TYPE_BLOB ->
-                                        statement.bindBlob(index, blobBindings[index]!!)
-                                    COLUMN_TYPE_NULL ->
-                                        statement.bindNull(index)
-                                }
-                            }
-                        }
-
-                        override val argCount: Int
-                            get() = bindingTypes.size
-                    }
-                )
-            }
-            return requireNotNull(cursor).moveToNext()
+            ensureCursor()
+            return checkNotNull(cursor).moveToNext()
         }
 
         override fun reset() {
@@ -197,6 +172,7 @@
 
         override fun close() {
             if (!isClosed) {
+                clearBindings()
                 reset()
             }
             isClosed = true
@@ -231,6 +207,37 @@
             }
         }
 
+        private fun ensureCursor() {
+            if (cursor == null) {
+                cursor = db.query(
+                    object : SupportSQLiteQuery {
+                        override val sql: String
+                            get() = this@SupportAndroidSQLiteStatement.sql
+
+                        override fun bindTo(statement: SupportSQLiteProgram) {
+                            for (index in 1 until bindingTypes.size) {
+                                when (bindingTypes[index]) {
+                                    COLUMN_TYPE_LONG ->
+                                        statement.bindLong(index, longBindings[index])
+                                    COLUMN_TYPE_DOUBLE ->
+                                        statement.bindDouble(index, doubleBindings[index])
+                                    COLUMN_TYPE_STRING ->
+                                        statement.bindString(index, stringBindings[index]!!)
+                                    COLUMN_TYPE_BLOB ->
+                                        statement.bindBlob(index, blobBindings[index]!!)
+                                    COLUMN_TYPE_NULL ->
+                                        statement.bindNull(index)
+                                }
+                            }
+                        }
+
+                        override val argCount: Int
+                            get() = bindingTypes.size
+                    }
+                )
+            }
+        }
+
         private fun throwIfNoRow(): Cursor {
             return cursor ?: throwSQLiteException(ResultCode.SQLITE_MISUSE, "no row")
         }
diff --git a/room/room-runtime/src/androidMain/kotlin/androidx/room/util/DBUtil.android.kt b/room/room-runtime/src/androidMain/kotlin/androidx/room/util/DBUtil.android.kt
index 1361804..d4c0a75 100644
--- a/room/room-runtime/src/androidMain/kotlin/androidx/room/util/DBUtil.android.kt
+++ b/room/room-runtime/src/androidMain/kotlin/androidx/room/util/DBUtil.android.kt
@@ -27,12 +27,42 @@
 import androidx.annotation.RestrictTo
 import androidx.room.RoomDatabase
 import androidx.room.driver.SupportSQLiteConnection
+import androidx.sqlite.SQLiteStatement
 import androidx.sqlite.db.SupportSQLiteDatabase
 import androidx.sqlite.db.SupportSQLiteQuery
 import java.io.File
 import java.io.FileInputStream
 import java.io.IOException
 import java.nio.ByteBuffer
+import kotlinx.coroutines.runBlocking
+
+/**
+ * Performs a single database read query operation.
+ */
+@RestrictTo(RestrictTo.Scope.LIBRARY_GROUP_PREFIX)
+fun <R> performReadBlocking(
+    db: RoomDatabase,
+    sql: String,
+    block: (SQLiteStatement) -> R
+): R {
+    db.assertNotMainThread()
+    db.assertNotSuspendingTransaction()
+    return runBlocking { db.perform(isReadOnly = true, sql, block) }
+}
+
+/**
+ * Performs a single database read query transaction operation.
+ */
+@RestrictTo(RestrictTo.Scope.LIBRARY_GROUP_PREFIX)
+fun <R> performReadTransactionBlocking(
+    db: RoomDatabase,
+    sql: String,
+    block: (SQLiteStatement) -> R
+): R {
+    db.assertNotMainThread()
+    db.assertNotSuspendingTransaction()
+    return runBlocking { db.performTransaction(isReadOnly = true) { it.usePrepared(sql, block) } }
+}
 
 /**
  * Performs the SQLiteQuery on the given database.
diff --git a/room/room-runtime/src/androidMain/kotlin/androidx/room/util/FtsTableInfo.android.kt b/room/room-runtime/src/androidMain/kotlin/androidx/room/util/FtsTableInfo.android.kt
index 89efc38..55d3ce3 100644
--- a/room/room-runtime/src/androidMain/kotlin/androidx/room/util/FtsTableInfo.android.kt
+++ b/room/room-runtime/src/androidMain/kotlin/androidx/room/util/FtsTableInfo.android.kt
@@ -16,66 +16,45 @@
 package androidx.room.util
 
 import androidx.annotation.RestrictTo
-import androidx.annotation.VisibleForTesting
 import androidx.room.driver.SupportSQLiteConnection
 import androidx.sqlite.SQLiteConnection
 import androidx.sqlite.db.SupportSQLiteDatabase
-import java.util.ArrayDeque
 
 /**
  * A data class that holds the information about an FTS table.
  *
  */
 @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP_PREFIX)
-class FtsTableInfo(
+actual class FtsTableInfo(
     /**
      * The table name
      */
     @JvmField
-    val name: String,
+    actual val name: String,
 
     /**
      * The column names
      */
     @JvmField
-    val columns: Set<String>,
+    actual val columns: Set<String>,
 
     /**
      * The set of options. Each value in the set contains the option in the following format:
      * <key, value>.
      */
     @JvmField
-    val options: Set<String>
+    actual val options: Set<String>
 ) {
-    constructor(name: String, columns: Set<String>, createSql: String) :
-        this(name, columns, parseOptions(createSql))
+    actual constructor(name: String, columns: Set<String>, createSql: String) :
+        this(name, columns, parseFtsOptions(createSql))
 
-    override fun equals(other: Any?): Boolean {
-        if (this === other) return true
-        if (other !is FtsTableInfo) return false
-        val that = other
-        if (name != that.name) return false
-        if (columns != that.columns) return false
-        return options == that.options
-    }
+    override fun equals(other: Any?) = equalsCommon(other)
 
-    override fun hashCode(): Int {
-        var result = name.hashCode()
-        result = 31 * result + (columns.hashCode())
-        result = 31 * result + (options.hashCode())
-        return result
-    }
+    override fun hashCode() = hashCodeCommon()
 
-    override fun toString(): String {
-        return ("FtsTableInfo{name='$name', columns=$columns, options=$options'}")
-    }
+    override fun toString() = toStringCommon()
 
-    companion object {
-        // A set of valid FTS Options
-        private val FTS_OPTIONS = arrayOf(
-            "tokenize=", "compress=", "content=", "languageid=", "matchinfo=", "notindexed=",
-            "order=", "prefix=", "uncompress="
-        )
+    actual companion object {
 
         /**
          * Reads the table information from the given database.
@@ -86,107 +65,21 @@
          */
         @JvmStatic
         fun read(database: SupportSQLiteDatabase, tableName: String): FtsTableInfo {
-            val columns = readColumns(database, tableName)
-            val options = readOptions(database, tableName)
-            return FtsTableInfo(tableName, columns, options)
-        }
-
-        @JvmStatic
-        fun read(connection: SQLiteConnection, tableName: String): FtsTableInfo {
-            if (connection is SupportSQLiteConnection) {
-                return read(connection.db, tableName)
-            } else {
-                TODO("Not yet migrated to use SQLiteDriver")
-            }
-        }
-
-        private fun readColumns(database: SupportSQLiteDatabase, tableName: String): Set<String> {
-            return buildSet {
-                database.query("PRAGMA table_info(`$tableName`)").useCursor { cursor ->
-                    if (cursor.columnCount > 0) {
-                        val nameIndex = cursor.getColumnIndex("name")
-                        while (cursor.moveToNext()) {
-                            add(cursor.getString(nameIndex))
-                        }
-                    }
-                }
-            }
-        }
-
-        private fun readOptions(database: SupportSQLiteDatabase, tableName: String): Set<String> {
-            val sql = database.query(
-                "SELECT * FROM sqlite_master WHERE `name` = '$tableName'"
-            ).useCursor { cursor ->
-                if (cursor.moveToFirst()) {
-                    cursor.getString(cursor.getColumnIndexOrThrow("sql"))
-                } else {
-                    ""
-                }
-            }
-            return parseOptions(sql)
+            return read(SupportSQLiteConnection(database), tableName)
         }
 
         /**
-         * Parses FTS options from the create statement of an FTS table.
+         * Reads the table information from the given database.
          *
-         * This method assumes the given create statement is a valid well-formed SQLite statement as
-         * defined in the [CREATE VIRTUAL TABLE
-         * syntax diagram](https://www.sqlite.org/lang_createvtab.html).
-         *
-         * @param createStatement the "CREATE VIRTUAL TABLE" statement.
-         * @return the set of FTS option key and values in the create statement.
+         * @param connection The database connection to read the information from.
+         * @param tableName The table name.
+         * @return A FtsTableInfo containing the columns and options for the provided table name.
          */
-        @VisibleForTesting
         @JvmStatic
-        fun parseOptions(createStatement: String): Set<String> {
-            if (createStatement.isEmpty()) {
-                return emptySet()
-            }
-
-            // Module arguments are within the parenthesis followed by the module name.
-            val argsString = createStatement.substring(
-                createStatement.indexOf('(') + 1,
-                createStatement.lastIndexOf(')')
-            )
-
-            // Split the module argument string by the comma delimiter, keeping track of quotation
-            // so that if the delimiter is found within a string literal we don't substring at the
-            // wrong index. SQLite supports four ways of quoting keywords, see:
-            // https://www.sqlite.org/lang_keywords.html
-            val args = mutableListOf<String>()
-            val quoteStack = ArrayDeque<Char>()
-            var lastDelimiterIndex = -1
-            argsString.forEachIndexed { i, value ->
-                when (value) {
-                    '\'', '"', '`' ->
-                        if (quoteStack.isEmpty()) {
-                            quoteStack.push(value)
-                        } else if (quoteStack.peek() == value) {
-                            quoteStack.pop()
-                        }
-                    '[' -> if (quoteStack.isEmpty()) {
-                        quoteStack.push(value)
-                    }
-                    ']' -> if (!quoteStack.isEmpty() && quoteStack.peek() == '[') {
-                        quoteStack.pop()
-                    }
-                    ',' -> if (quoteStack.isEmpty()) {
-                        args.add(argsString.substring(lastDelimiterIndex + 1, i).trim { it <= ' ' })
-                        lastDelimiterIndex = i
-                    }
-                }
-            }
-
-            // Add final argument.
-            args.add(argsString.substring(lastDelimiterIndex + 1).trim())
-
-            // Match args against valid options, otherwise they are column definitions.
-            val options = args.filter { arg ->
-                FTS_OPTIONS.any { validOption ->
-                    arg.startsWith(validOption)
-                }
-            }.toSet()
-            return options
+        actual fun read(connection: SQLiteConnection, tableName: String): FtsTableInfo {
+            val columns = readFtsColumns(connection, tableName)
+            val options = readFtsOptions(connection, tableName)
+            return FtsTableInfo(tableName, columns, options)
         }
     }
 }
diff --git a/room/room-runtime/src/androidMain/kotlin/androidx/room/util/StatementUtil.android.kt b/room/room-runtime/src/androidMain/kotlin/androidx/room/util/StatementUtil.android.kt
new file mode 100644
index 0000000..6b4d5d2
--- /dev/null
+++ b/room/room-runtime/src/androidMain/kotlin/androidx/room/util/StatementUtil.android.kt
@@ -0,0 +1,68 @@
+/*
+ * Copyright 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+@file:JvmMultifileClass
+@file:JvmName("SQLiteStatementUtil")
+
+package androidx.room.util
+
+import android.os.Build
+import androidx.sqlite.SQLiteStatement
+
+/**
+ * Returns the zero-based index for the given column name, or -1 if the column doesn't exist.
+ *
+ * The implementation also contains Android-specific patches to workaround issues on older devices.
+ */
+internal actual fun SQLiteStatement.getColumnIndex(name: String): Int {
+    var index = this.columnIndexOf(name)
+    if (index >= 0) {
+        return index
+    }
+    index = this.columnIndexOf("`$name`")
+    return if (index >= 0) {
+        index
+    } else {
+        this.findColumnIndexBySuffix(name)
+    }
+}
+
+/**
+ * Finds a column by name by appending `.` in front of it and checking by suffix match. Also checks
+ * for the version wrapped with `` (backticks)m a workaround for b/157261134 for API levels 25 and
+ * below e.g. "foo" will match "any.foo" and "`any.foo`".
+ */
+private fun SQLiteStatement.findColumnIndexBySuffix(name: String): Int {
+    // This workaround is only on APIs < 26. So just return not found on newer APIs
+    if (Build.VERSION.SDK_INT > Build.VERSION_CODES.N_MR1 || name.isEmpty()) {
+        return -1
+    }
+    val columnCount = getColumnCount()
+    val dotSuffix = ".$name"
+    val backtickSuffix = ".$name`"
+    for (i in 0 until columnCount) {
+        val columnName = getColumnName(i)
+        // Do not check if column name is not long enough. 1 char for table name, 1 char for '.'
+        if (columnName.length >= name.length + 2) {
+            if (columnName.endsWith(dotSuffix)) {
+                return i
+            } else if (columnName[0] == '`' && columnName.endsWith(backtickSuffix)) {
+                return i
+            }
+        }
+    }
+    return -1
+}
diff --git a/room/room-runtime/src/androidMain/kotlin/androidx/room/util/TableInfo.android.kt b/room/room-runtime/src/androidMain/kotlin/androidx/room/util/TableInfo.android.kt
index 4e6ddf6..e77a2b8 100644
--- a/room/room-runtime/src/androidMain/kotlin/androidx/room/util/TableInfo.android.kt
+++ b/room/room-runtime/src/androidMain/kotlin/androidx/room/util/TableInfo.android.kt
@@ -15,18 +15,13 @@
  */
 package androidx.room.util
 
-import android.database.Cursor
 import android.os.Build
 import androidx.annotation.IntDef
 import androidx.annotation.RestrictTo
-import androidx.annotation.VisibleForTesting
-import androidx.room.ColumnInfo
 import androidx.room.ColumnInfo.SQLiteTypeAffinity
 import androidx.room.driver.SupportSQLiteConnection
 import androidx.sqlite.SQLiteConnection
 import androidx.sqlite.db.SupportSQLiteDatabase
-import java.util.Locale
-import java.util.TreeMap
 
 /**
  * A data class that holds the information about a table.
@@ -36,22 +31,20 @@
  * documentation for more details.
  *
  * Even though SQLite column names are case insensitive, this class uses case sensitive matching.
- *
  */
 @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP_PREFIX)
-// if you change this class, you must change TableInfoValidationWriter.kt
-class TableInfo(
+actual class TableInfo actual constructor(
     /**
      * The table name.
      */
     @JvmField
-    val name: String,
+    actual val name: String,
     @JvmField
-    val columns: Map<String, Column>,
+    actual val columns: Map<String, Column>,
     @JvmField
-    val foreignKeys: Set<ForeignKey>,
+    actual val foreignKeys: Set<ForeignKey>,
     @JvmField
-    val indices: Set<Index>? = null
+    actual val indices: Set<Index>?
 ) {
     /**
      * Identifies from where the info object was created.
@@ -63,60 +56,36 @@
     /**
      * For backward compatibility with dbs created with older versions.
      */
-    @SuppressWarnings("unused")
+    @Deprecated("No longer used by generated code.")
     constructor(
         name: String,
         columns: Map<String, Column>,
         foreignKeys: Set<ForeignKey>
     ) : this(name, columns, foreignKeys, emptySet<Index>())
 
-    override fun equals(other: Any?): Boolean {
-        if (this === other) return true
-        if (other !is TableInfo) return false
-        if (name != other.name) return false
-        if (columns != other.columns) {
-            return false
-        }
-        if (foreignKeys != other.foreignKeys) {
-            return false
-        }
-        return if (indices == null || other.indices == null) {
-            // if one us is missing index information, seems like we couldn't acquire the
-            // information so we better skip.
-            true
-        } else indices == other.indices
-    }
+    actual override fun equals(other: Any?) = equalsCommon(other)
 
-    override fun hashCode(): Int {
-        var result = name.hashCode()
-        result = 31 * result + columns.hashCode()
-        result = 31 * result + foreignKeys.hashCode()
-        // skip index, it is not reliable for comparison.
-        return result
-    }
+    actual override fun hashCode() = hashCodeCommon()
 
-    override fun toString(): String {
-        return ("TableInfo{name='$name', columns=$columns, foreignKeys=$foreignKeys, " +
-            "indices=$indices}")
-    }
+    actual override fun toString() = toStringCommon()
 
-    companion object {
+    actual companion object {
         /**
          * Identifier for when the info is created from an unknown source.
          */
-        const val CREATED_FROM_UNKNOWN = 0
+        actual const val CREATED_FROM_UNKNOWN = 0
 
         /**
          * Identifier for when the info is created from an entity definition, such as generated code
          * by the compiler or at runtime from a schema bundle, parsed from a schema JSON file.
          */
-        const val CREATED_FROM_ENTITY = 1
+        actual const val CREATED_FROM_ENTITY = 1
 
         /**
          * Identifier for when the info is created from the database itself, reading information
          * from a PRAGMA, such as table_info.
          */
-        const val CREATED_FROM_DATABASE = 2
+        actual const val CREATED_FROM_DATABASE = 2
 
         /**
          * Reads the table information from the given database.
@@ -125,63 +94,72 @@
          * @param tableName The table name.
          * @return A TableInfo containing the schema information for the provided table name.
          */
+        @Deprecated("No longer used by generated code.")
         @JvmStatic
         fun read(database: SupportSQLiteDatabase, tableName: String): TableInfo {
-            return readTableInfo(
-                database = database,
-                tableName = tableName
-            )
+            return read(SupportSQLiteConnection(database), tableName)
         }
 
+        /**
+         * Reads the table information from the given database.
+         *
+         * @param connection The database connection to read the information from.
+         * @param tableName The table name.
+         * @return A TableInfo containing the schema information for the provided table name.
+         */
         @JvmStatic
-        fun read(connection: SQLiteConnection, tableName: String): TableInfo {
-            if (connection is SupportSQLiteConnection) {
-                return read(connection.db, tableName)
-            } else {
-                TODO("Not yet migrated to use SQLiteDriver")
-            }
+        actual fun read(connection: SQLiteConnection, tableName: String): TableInfo {
+            return readTableInfo(connection, tableName)
         }
     }
 
     /**
      * Holds the information about a database column.
      */
-    class Column(
+    @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP_PREFIX)
+    actual class Column actual constructor(
         /**
          * The column name.
          */
         @JvmField
-        val name: String,
+        actual val name: String,
         /**
          * The column type affinity.
          */
         @JvmField
-        val type: String,
+        actual val type: String,
         /**
          * Whether or not the column can be NULL.
          */
         @JvmField
-        val notNull: Boolean,
+        actual val notNull: Boolean,
         @JvmField
-        val primaryKeyPosition: Int,
+        actual val primaryKeyPosition: Int,
         @JvmField
-        val defaultValue: String?,
+        actual val defaultValue: String?,
         @CreatedFrom
         @JvmField
-        val createdFrom: Int
+        actual val createdFrom: Int
     ) {
         /**
          * The column type after it is normalized to one of the basic types according to
          * https://www.sqlite.org/datatype3.html Section 3.1.
          *
-         *
          * This is the value Room uses for equality check.
          */
         @SQLiteTypeAffinity
         @JvmField
-        val affinity: Int = findAffinity(type)
+        actual val affinity: Int = findAffinity(type)
 
-        @Deprecated("Use {@link Column#Column(String, String, boolean, int, String, int)} instead.")
+        /**
+         * Returns whether this column is part of the primary key or not.
+         *
+         * @return True if this column is part of the primary key, false otherwise.
+         */
+        actual val isPrimaryKey: Boolean
+            get() = primaryKeyPosition > 0
+
+        @Deprecated("No longer used by generated code.")
         constructor(name: String, type: String, notNull: Boolean, primaryKeyPosition: Int) : this(
             name,
             type,
@@ -191,230 +169,55 @@
             CREATED_FROM_UNKNOWN
         )
 
-        /**
-         * Implements https://www.sqlite.org/datatype3.html section 3.1
-         *
-         * @param type The type that was given to the sqlite
-         * @return The normalized type which is one of the 5 known affinities
-         */
-        @SQLiteTypeAffinity
-        private fun findAffinity(type: String?): Int {
-            if (type == null) {
-                return ColumnInfo.BLOB
-            }
-            val uppercaseType = type.uppercase(Locale.US)
-            if (uppercaseType.contains("INT")) {
-                return ColumnInfo.INTEGER
-            }
-            if (uppercaseType.contains("CHAR") ||
-                uppercaseType.contains("CLOB") ||
-                uppercaseType.contains("TEXT")
-            ) {
-                return ColumnInfo.TEXT
-            }
-            if (uppercaseType.contains("BLOB")) {
-                return ColumnInfo.BLOB
-            }
-            if (uppercaseType.contains("REAL") ||
-                uppercaseType.contains("FLOA") ||
-                uppercaseType.contains("DOUB")
-            ) {
-                return ColumnInfo.REAL
-            }
-            // sqlite returns NUMERIC here but it is like a catch all. We already
-            // have UNDEFINED so it is better to use UNDEFINED for consistency.
-            return ColumnInfo.UNDEFINED
-        }
-
         companion object {
-            /**
-             * Checks if the default values provided match. Handles the special case in which the
-             * default value is surrounded by parenthesis (e.g. encountered in b/182284899).
-             *
-             * Surrounding parenthesis are removed by SQLite when reading from the database, hence
-             * this function will check if they are present in the actual value, if so, it will
-             * compare the two values by ignoring the surrounding parenthesis.
-             *
-             */
-            @VisibleForTesting
             @JvmStatic
-            fun defaultValueEquals(current: String, other: String?): Boolean {
-                if (current == other) {
-                    return true
-                } else if (containsSurroundingParenthesis(current)) {
-                    return current.substring(1, current.length - 1).trim() == other
-                }
-                return false
-            }
-
-            /**
-             * Checks for potential surrounding parenthesis, if found, removes them and checks if
-             * remaining paranthesis are balanced. If so, the surrounding parenthesis are redundant,
-             * and returns true.
-             */
-            private fun containsSurroundingParenthesis(current: String): Boolean {
-                if (current.isEmpty()) {
-                    return false
-                }
-                var surroundingParenthesis = 0
-                current.forEachIndexed { i, c ->
-                    if (i == 0 && c != '(') {
-                        return false
-                    }
-                    if (c == '(') {
-                        surroundingParenthesis++
-                    } else if (c == ')') {
-                        surroundingParenthesis--
-                        if (surroundingParenthesis == 0 && i != current.length - 1) {
-                            return false
-                        }
-                    }
-                }
-                return surroundingParenthesis == 0
-            }
+            fun defaultValueEquals(current: String, other: String?) =
+                defaultValueEqualsCommon(current, other)
         }
 
-        override fun equals(other: Any?): Boolean {
-            if (this === other) return true
-            if (other !is Column) return false
-            if (Build.VERSION.SDK_INT >= 20) {
-                if (primaryKeyPosition != other.primaryKeyPosition) return false
-            } else {
-                if (isPrimaryKey != other.isPrimaryKey) return false
-            }
-            if (name != other.name) return false
-            if (notNull != other.notNull) return false
-            // Only validate default value if it was defined in an entity, i.e. if the info
-            // from the compiler itself has it. b/136019383
-            if (
-                createdFrom == CREATED_FROM_ENTITY &&
-                    other.createdFrom == CREATED_FROM_DATABASE &&
-                    defaultValue != null &&
-                    !defaultValueEquals(defaultValue, other.defaultValue)
-            ) {
-                return false
-            } else if (
-                createdFrom == CREATED_FROM_DATABASE &&
-                other.createdFrom == CREATED_FROM_ENTITY &&
-                other.defaultValue != null &&
-                !defaultValueEquals(other.defaultValue, defaultValue)
-            ) {
-                return false
-            } else if (
-                createdFrom != CREATED_FROM_UNKNOWN &&
-                createdFrom == other.createdFrom &&
-                (if (defaultValue != null)
-                    !defaultValueEquals(defaultValue, other.defaultValue)
-                else other.defaultValue != null)
-            ) {
-                return false
-            }
-            return affinity == other.affinity
-        }
+        actual override fun equals(other: Any?) = equalsCommon(other)
 
-        /**
-         * Returns whether this column is part of the primary key or not.
-         *
-         * @return True if this column is part of the primary key, false otherwise.
-         */
-        val isPrimaryKey: Boolean
-            get() = primaryKeyPosition > 0
+        actual override fun hashCode() = hashCodeCommon()
 
-        override fun hashCode(): Int {
-            var result = name.hashCode()
-            result = 31 * result + affinity
-            result = 31 * result + if (notNull) 1231 else 1237
-            result = 31 * result + primaryKeyPosition
-            // Default value is not part of the hashcode since we conditionally check it for
-            // equality which would break the equals + hashcode contract.
-            // result = 31 * result + (defaultValue != null ? defaultValue.hashCode() : 0);
-            return result
-        }
-
-        override fun toString(): String {
-            return ("Column{name='$name', type='$type', affinity='$affinity', " +
-                "notNull=$notNull, primaryKeyPosition=$primaryKeyPosition, " +
-                "defaultValue='${defaultValue ?: "undefined"}'}")
-        }
+        actual override fun toString() = toStringCommon()
     }
 
     /**
      * Holds the information about an SQLite foreign key
-     *
      */
     @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP_PREFIX)
-    class ForeignKey(
+    actual class ForeignKey actual constructor(
         @JvmField
-        val referenceTable: String,
+        actual val referenceTable: String,
         @JvmField
-        val onDelete: String,
+        actual val onDelete: String,
         @JvmField
-        val onUpdate: String,
+        actual val onUpdate: String,
         @JvmField
-        val columnNames: List<String>,
+        actual val columnNames: List<String>,
         @JvmField
-        val referenceColumnNames: List<String>
+        actual val referenceColumnNames: List<String>
     ) {
-        override fun equals(other: Any?): Boolean {
-            if (this === other) return true
-            if (other !is ForeignKey) return false
-            if (referenceTable != other.referenceTable) return false
-            if (onDelete != other.onDelete) return false
-            if (onUpdate != other.onUpdate) return false
-            return if (columnNames != other.columnNames) false else referenceColumnNames ==
-                other.referenceColumnNames
-        }
+        actual override fun equals(other: Any?) = equalsCommon(other)
 
-        override fun hashCode(): Int {
-            var result = referenceTable.hashCode()
-            result = 31 * result + onDelete.hashCode()
-            result = 31 * result + onUpdate.hashCode()
-            result = 31 * result + columnNames.hashCode()
-            result = 31 * result + referenceColumnNames.hashCode()
-            return result
-        }
+        actual override fun hashCode() = hashCodeCommon()
 
-        override fun toString(): String {
-            return ("ForeignKey{referenceTable='$referenceTable', onDelete='$onDelete +', " +
-                "onUpdate='$onUpdate', columnNames=$columnNames, " +
-                "referenceColumnNames=$referenceColumnNames}")
-        }
-    }
-
-    /**
-     * Temporary data holder for a foreign key row in the pragma result. We need this to ensure
-     * sorting in the generated foreign key object.
-     */
-    internal class ForeignKeyWithSequence(
-        val id: Int,
-        val sequence: Int,
-        val from: String,
-        val to: String
-    ) : Comparable<ForeignKeyWithSequence> {
-        override fun compareTo(other: ForeignKeyWithSequence): Int {
-            val idCmp = id - other.id
-            return if (idCmp == 0) {
-                sequence - other.sequence
-            } else {
-                idCmp
-            }
-        }
+        actual override fun toString() = toStringCommon()
     }
 
     /**
      * Holds the information about an SQLite index
-     *
      */
     @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP_PREFIX)
-    class Index(
+    actual class Index actual constructor(
         @JvmField
-        val name: String,
+        actual val name: String,
         @JvmField
-        val unique: Boolean,
+        actual val unique: Boolean,
         @JvmField
-        val columns: List<String>,
+        actual val columns: List<String>,
         @JvmField
-        var orders: List<String>
+        actual var orders: List<String>
     ) {
         init {
             orders = orders.ifEmpty {
@@ -422,12 +225,12 @@
             }
         }
 
-        companion object {
+        actual companion object {
             // should match the value in Index.kt
-            const val DEFAULT_PREFIX = "index_"
+            actual const val DEFAULT_PREFIX = "index_"
         }
 
-        @Deprecated("Use {@link #Index(String, boolean, List, List)}")
+        @Deprecated("No longer used by generated code.")
         constructor(name: String, unique: Boolean, columns: List<String>) : this(
             name,
             unique,
@@ -435,221 +238,22 @@
             List<String>(columns.size) { androidx.room.Index.Order.ASC.name }
         )
 
-        override fun equals(other: Any?): Boolean {
-            if (this === other) return true
-            if (other !is Index) return false
-            if (unique != other.unique) {
-                return false
-            }
-            if (columns != other.columns) {
-                return false
-            }
-            if (orders != other.orders) {
-                return false
-            }
-            return if (name.startsWith(DEFAULT_PREFIX)) {
-                other.name.startsWith(DEFAULT_PREFIX)
-            } else {
-                name == other.name
-            }
-        }
+        actual override fun equals(other: Any?) = equalsCommon(other)
 
-        override fun hashCode(): Int {
-            var result = if (name.startsWith(DEFAULT_PREFIX)) {
-                DEFAULT_PREFIX.hashCode()
-            } else {
-                name.hashCode()
-            }
-            result = 31 * result + if (unique) 1 else 0
-            result = 31 * result + columns.hashCode()
-            result = 31 * result + orders.hashCode()
-            return result
-        }
+        actual override fun hashCode() = hashCodeCommon()
 
-        override fun toString(): String {
-            return ("Index{name='$name', unique=$unique, columns=$columns, orders=$orders'}")
-        }
-    }
-}
-
-internal fun readTableInfo(database: SupportSQLiteDatabase, tableName: String): TableInfo {
-    val columns = readColumns(database, tableName)
-    val foreignKeys = readForeignKeys(database, tableName)
-    val indices = readIndices(database, tableName)
-    return TableInfo(tableName, columns, foreignKeys, indices)
-}
-
-private fun readForeignKeys(
-    database: SupportSQLiteDatabase,
-    tableName: String
-): Set<TableInfo.ForeignKey> {
-    // this seems to return everything in order but it is not documented so better be safe
-    database.query("PRAGMA foreign_key_list(`$tableName`)").useCursor { cursor ->
-        val idColumnIndex = cursor.getColumnIndex("id")
-        val seqColumnIndex = cursor.getColumnIndex("seq")
-        val tableColumnIndex = cursor.getColumnIndex("table")
-        val onDeleteColumnIndex = cursor.getColumnIndex("on_delete")
-        val onUpdateColumnIndex = cursor.getColumnIndex("on_update")
-        val ordered = readForeignKeyFieldMappings(cursor)
-
-        // Reset cursor as readForeignKeyFieldMappings has moved it
-        cursor.moveToPosition(-1)
-        return buildSet {
-            while (cursor.moveToNext()) {
-                val seq = cursor.getInt(seqColumnIndex)
-                if (seq != 0) {
-                    continue
-                }
-                val id = cursor.getInt(idColumnIndex)
-                val myColumns = mutableListOf<String>()
-                val refColumns = mutableListOf<String>()
-
-                ordered.filter {
-                    it.id == id
-                }.forEach { key ->
-                    myColumns.add(key.from)
-                    refColumns.add(key.to)
-                }
-
-                add(
-                    TableInfo.ForeignKey(
-                        referenceTable = cursor.getString(tableColumnIndex),
-                        onDelete = cursor.getString(onDeleteColumnIndex),
-                        onUpdate = cursor.getString(onUpdateColumnIndex),
-                        columnNames = myColumns,
-                        referenceColumnNames = refColumns
-                    )
-                )
-            }
-        }
-    }
-}
-
-private fun readForeignKeyFieldMappings(cursor: Cursor): List<TableInfo.ForeignKeyWithSequence> {
-    val idColumnIndex = cursor.getColumnIndex("id")
-    val seqColumnIndex = cursor.getColumnIndex("seq")
-    val fromColumnIndex = cursor.getColumnIndex("from")
-    val toColumnIndex = cursor.getColumnIndex("to")
-
-    return buildList {
-        while (cursor.moveToNext()) {
-            add(
-                TableInfo.ForeignKeyWithSequence(
-                    id = cursor.getInt(idColumnIndex),
-                    sequence = cursor.getInt(seqColumnIndex),
-                    from = cursor.getString(fromColumnIndex),
-                    to = cursor.getString(toColumnIndex)
-                )
-            )
-        }
-    }.sorted()
-}
-
-private fun readColumns(
-    database: SupportSQLiteDatabase,
-    tableName: String
-): Map<String, TableInfo.Column> {
-    database.query("PRAGMA table_info(`$tableName`)").useCursor { cursor ->
-        if (cursor.columnCount <= 0) {
-            return emptyMap()
-        }
-
-        val nameIndex = cursor.getColumnIndex("name")
-        val typeIndex = cursor.getColumnIndex("type")
-        val notNullIndex = cursor.getColumnIndex("notnull")
-        val pkIndex = cursor.getColumnIndex("pk")
-        val defaultValueIndex = cursor.getColumnIndex("dflt_value")
-
-        return buildMap {
-            while (cursor.moveToNext()) {
-                val name = cursor.getString(nameIndex)
-                val type = cursor.getString(typeIndex)
-                val notNull = 0 != cursor.getInt(notNullIndex)
-                val primaryKeyPosition = cursor.getInt(pkIndex)
-                val defaultValue = cursor.getString(defaultValueIndex)
-                put(
-                    key = name,
-                    value = TableInfo.Column(
-                        name = name,
-                        type = type,
-                        notNull = notNull,
-                        primaryKeyPosition = primaryKeyPosition,
-                        defaultValue = defaultValue,
-                        createdFrom = TableInfo.CREATED_FROM_DATABASE
-                    )
-                )
-            }
-        }
+        actual override fun toString() = toStringCommon()
     }
 }
 
 /**
- * @return null if we cannot read the indices due to older sqlite implementations.
+ * Checks if the primary key match.
  */
-private fun readIndices(database: SupportSQLiteDatabase, tableName: String): Set<TableInfo.Index>? {
-    database.query("PRAGMA index_list(`$tableName`)").useCursor { cursor ->
-        val nameColumnIndex = cursor.getColumnIndex("name")
-        val originColumnIndex = cursor.getColumnIndex("origin")
-        val uniqueIndex = cursor.getColumnIndex("unique")
-        if (nameColumnIndex == -1 || originColumnIndex == -1 || uniqueIndex == -1) {
-            // we cannot read them so better not validate any index.
-            return null
-        }
-        return buildSet {
-            while (cursor.moveToNext()) {
-                val origin = cursor.getString(originColumnIndex)
-                if ("c" != origin) {
-                    // Ignore auto-created indices
-                    continue
-                }
-                val name = cursor.getString(nameColumnIndex)
-                val unique = cursor.getInt(uniqueIndex) == 1
-                // Read index but if we cannot read it properly so better not read it
-                val index = readIndex(database, name, unique) ?: return null
-                add(index)
-            }
-        }
+internal actual fun TableInfo.Column.equalsInPrimaryKey(other: TableInfo.Column): Boolean {
+    if (Build.VERSION.SDK_INT >= 20) {
+        if (primaryKeyPosition != other.primaryKeyPosition) return false
+    } else {
+        if (isPrimaryKey != other.isPrimaryKey) return false
     }
-}
-
-/**
- * @return null if we cannot read the index due to older sqlite implementations.
- */
-private fun readIndex(
-    database: SupportSQLiteDatabase,
-    name: String,
-    unique: Boolean
-): TableInfo.Index? {
-    return database.query("PRAGMA index_xinfo(`$name`)").useCursor { cursor ->
-        val seqnoColumnIndex = cursor.getColumnIndex("seqno")
-        val cidColumnIndex = cursor.getColumnIndex("cid")
-        val nameColumnIndex = cursor.getColumnIndex("name")
-        val descColumnIndex = cursor.getColumnIndex("desc")
-        if (
-            seqnoColumnIndex == -1 ||
-            cidColumnIndex == -1 ||
-            nameColumnIndex == -1 ||
-            descColumnIndex == -1
-        ) {
-            // we cannot read them so better not validate any index.
-            return null
-        }
-        val columnsMap = TreeMap<Int, String>()
-        val ordersMap = TreeMap<Int, String>()
-        while (cursor.moveToNext()) {
-            val cid = cursor.getInt(cidColumnIndex)
-            if (cid < 0) {
-                // Ignore SQLite row ID
-                continue
-            }
-            val seq = cursor.getInt(seqnoColumnIndex)
-            val columnName = cursor.getString(nameColumnIndex)
-            val order = if (cursor.getInt(descColumnIndex) > 0) "DESC" else "ASC"
-            columnsMap[seq] = columnName
-            ordersMap[seq] = order
-        }
-        val columns = columnsMap.values.toList()
-        val orders = ordersMap.values.toList()
-        TableInfo.Index(name, unique, columns, orders)
-    }
+    return true
 }
diff --git a/room/room-runtime/src/androidMain/kotlin/androidx/room/util/ViewInfo.android.kt b/room/room-runtime/src/androidMain/kotlin/androidx/room/util/ViewInfo.android.kt
index 4b5d1a8..18a7f71 100644
--- a/room/room-runtime/src/androidMain/kotlin/androidx/room/util/ViewInfo.android.kt
+++ b/room/room-runtime/src/androidMain/kotlin/androidx/room/util/ViewInfo.android.kt
@@ -23,43 +23,30 @@
 /**
  * A data class that holds the information about a view.
  *
- *
  * This derives information from sqlite_master.
  *
- *
  * Even though SQLite column names are case insensitive, this class uses case sensitive matching.
- *
  */
 @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP_PREFIX)
-class ViewInfo(
+actual class ViewInfo actual constructor(
     /**
      * The view name
      */
     @JvmField
-    val name: String,
+    actual val name: String,
     /**
      * The SQL of CREATE VIEW.
      */
     @JvmField
-    val sql: String?
+    actual val sql: String?
 ) {
-    override fun equals(other: Any?): Boolean {
-        if (this === other) return true
-        if (other !is ViewInfo) return false
-        return ((name == other.name) && if (sql != null) sql == other.sql else other.sql == null)
-    }
+    actual override fun equals(other: Any?) = equalsCommon(other)
 
-    override fun hashCode(): Int {
-        var result = name.hashCode()
-        result = 31 * result + (sql?.hashCode() ?: 0)
-        return result
-    }
+    actual override fun hashCode() = hashCodeCommon()
 
-    override fun toString(): String {
-        return ("ViewInfo{" + "name='" + name + '\'' + ", sql='" + sql + '\'' + '}')
-    }
+    actual override fun toString() = toStringCommon()
 
-    companion object {
+    actual companion object {
         /**
          * Reads the view information from the given database.
          *
@@ -67,27 +54,22 @@
          * @param viewName The view name.
          * @return A ViewInfo containing the schema information for the provided view name.
          */
+        @Deprecated("No longer used by generated code.")
         @JvmStatic
         fun read(database: SupportSQLiteDatabase, viewName: String): ViewInfo {
-            return database.query(
-                "SELECT name, sql FROM sqlite_master " +
-                    "WHERE type = 'view' AND name = '$viewName'"
-            ).useCursor { cursor ->
-                if (cursor.moveToFirst()) {
-                    ViewInfo(cursor.getString(0), cursor.getString(1))
-                } else {
-                    ViewInfo(viewName, null)
-                }
-            }
+            return read(SupportSQLiteConnection(database), viewName)
         }
 
+        /**
+         * Reads the view information from the given database.
+         *
+         * @param connection The database connection to read the information from.
+         * @param viewName The view name.
+         * @return A ViewInfo containing the schema information for the provided view name.
+         */
         @JvmStatic
-        fun read(connection: SQLiteConnection, viewName: String): ViewInfo {
-            if (connection is SupportSQLiteConnection) {
-                return read(connection.db, viewName)
-            } else {
-                TODO("Not yet migrated to use SQLiteDriver")
-            }
+        actual fun read(connection: SQLiteConnection, viewName: String): ViewInfo {
+            return readViewInfo(connection, viewName)
         }
     }
 }
diff --git a/room/room-runtime/src/androidUnitTest/kotlin/androidx/room/InvalidationTrackerTest.kt b/room/room-runtime/src/androidUnitTest/kotlin/androidx/room/InvalidationTrackerTest.kt
index ac18a3e..317eccd 100644
--- a/room/room-runtime/src/androidUnitTest/kotlin/androidx/room/InvalidationTrackerTest.kt
+++ b/room/room-runtime/src/androidUnitTest/kotlin/androidx/room/InvalidationTrackerTest.kt
@@ -96,6 +96,7 @@
             mRoomDatabase, shadowTables, viewTables,
             "a", "B", "i", "C", "d"
         )
+        @Suppress("DEPRECATION")
         mTracker.internalInit(mSqliteDb)
         reset(mSqliteDb)
     }
diff --git a/room/room-runtime/src/androidUnitTest/kotlin/androidx/room/util/FtsTableInfoTest.java b/room/room-runtime/src/androidUnitTest/kotlin/androidx/room/util/FtsTableInfoTest.java
deleted file mode 100644
index f7c264d..0000000
--- a/room/room-runtime/src/androidUnitTest/kotlin/androidx/room/util/FtsTableInfoTest.java
+++ /dev/null
@@ -1,91 +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.util;
-
-import static org.hamcrest.CoreMatchers.is;
-import static org.hamcrest.MatcherAssert.assertThat;
-
-import org.junit.Test;
-import org.junit.runner.RunWith;
-import org.junit.runners.JUnit4;
-import org.mockito.internal.util.collections.Sets;
-
-import java.util.Set;
-
-@RunWith(JUnit4.class)
-public class FtsTableInfoTest {
-
-    @Test
-    public void test_parseOptions() {
-        assertOptions("CREATE VIRTUAL TABLE Book USING FTS4()");
-
-        assertOptions("CREATE VIRTUAL TABLE Book USING FTS4(  )");
-
-        assertOptions("CREATE VIRTUAL TABLE Book USING fts4(author)");
-
-        assertOptions("CREATE VIRTUAL TABLE Book USING FTS4(author, matchinfo=fts3)",
-                "matchinfo=fts3");
-
-        assertOptions("CREATE VIRTUAL TABLE \"Book\" USING FTS4(\"author\", "
-                        + "matchinfo=fts3)",
-                "matchinfo=fts3");
-
-        assertOptions("CREATE VIRTUAL TABLE `Fun'Names` USING FTS4(matchinfo=fts3)",
-                "matchinfo=fts3");
-
-        assertOptions("CREATE VIRTUAL TABLE `Fun'With'Names` USING FTS4(\"odd'column'\", "
-                        + "`odd'column'again`, [select], 'left[col]is`weird', matchinfo=fts3)",
-                "matchinfo=fts3");
-
-        assertOptions("CREATE VIRTUAL TABLE 'Book' USING FTS4('content', 'pages', "
-                        + "'isbn', notindexed=pages, notindexed=isbn)",
-                "notindexed=pages", "notindexed=isbn");
-
-        assertOptions("CREATE VIRTUAL TABLE `Book` USING FTS4(tokenize=porter, "
-                        + "`content`, `pages`, notindexed=pages)",
-                "tokenize=porter", "notindexed=pages");
-
-        assertOptions("CREATE VIRTUAL TABLE `Book` USING FTS4(tokenize=porter, "
-                        + "`content`, `pages`, notindexed=pages)",
-                "tokenize=porter", "notindexed=pages");
-
-        assertOptions("CREATE VIRTUAL TABLE `Book` USING FTS4(tokenize=unicode61 "
-                        + "\"tokenchars=,\")",
-                "tokenize=unicode61 \"tokenchars=,\"");
-
-        assertOptions("CREATE VIRTUAL TABLE `Book` USING FTS4(tokenize=unicode61 "
-                        + "`tokenchars=,`)",
-                "tokenize=unicode61 `tokenchars=,`");
-
-        assertOptions("CREATE VIRTUAL TABLE `Book` USING FTS4(tokenize=unicode61 "
-                        + "\"tokenchars=.=\" \"separators=X\", `pages`, notindexed=pages)",
-                "tokenize=unicode61 \"tokenchars=.=\" \"separators=X\"",
-                "notindexed=pages");
-
-        assertOptions("CREATE VIRTUAL TABLE `Book` USING FTS4(tokenize=porter, "
-                        + "`author`, languageid=`lid`, matchinfo=fts3, notindexed=`pages`, "
-                        + "order=desc, prefix=`2,4`)",
-                "tokenize=porter", "languageid=`lid`", "matchinfo=fts3",
-                "notindexed=`pages`", "order=desc", "prefix=`2,4`");
-    }
-
-    private void assertOptions(String createSql, String... options) {
-        Set<String> actualOptions = FtsTableInfo.parseOptions(createSql);
-        Set<String> expectedOptions = Sets.newSet(options);
-        assertThat(actualOptions, is(expectedOptions));
-    }
-}
diff --git a/room/room-runtime/src/androidUnitTest/kotlin/androidx/room/util/FtsTableInfoTest.kt b/room/room-runtime/src/androidUnitTest/kotlin/androidx/room/util/FtsTableInfoTest.kt
new file mode 100644
index 0000000..723aa8b
--- /dev/null
+++ b/room/room-runtime/src/androidUnitTest/kotlin/androidx/room/util/FtsTableInfoTest.kt
@@ -0,0 +1,92 @@
+/*
+ * 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.util
+
+import org.hamcrest.CoreMatchers
+import org.hamcrest.MatcherAssert
+import org.junit.Test
+import org.junit.runner.RunWith
+import org.junit.runners.JUnit4
+import org.mockito.internal.util.collections.Sets
+
+@RunWith(JUnit4::class)
+class FtsTableInfoTest {
+    @Test
+    fun test_parseOptions() {
+        assertOptions("CREATE VIRTUAL TABLE Book USING FTS4()")
+        assertOptions("CREATE VIRTUAL TABLE Book USING FTS4(  )")
+        assertOptions("CREATE VIRTUAL TABLE Book USING fts4(author)")
+        assertOptions(
+            "CREATE VIRTUAL TABLE Book USING FTS4(author, matchinfo=fts3)",
+            "matchinfo=fts3"
+        )
+        assertOptions(
+            "CREATE VIRTUAL TABLE \"Book\" USING FTS4(\"author\", matchinfo=fts3)",
+            "matchinfo=fts3"
+        )
+        assertOptions(
+            "CREATE VIRTUAL TABLE `Fun'Names` USING FTS4(matchinfo=fts3)",
+            "matchinfo=fts3"
+        )
+        assertOptions(
+            "CREATE VIRTUAL TABLE `Fun'With'Names` USING FTS4(\"odd'column'\", " +
+                "`odd'column'again`, [select], 'left[col]is`weird', matchinfo=fts3)",
+            "matchinfo=fts3"
+        )
+        assertOptions(
+            "CREATE VIRTUAL TABLE 'Book' USING FTS4('content', 'pages', " +
+                "'isbn', notindexed=pages, notindexed=isbn)",
+            "notindexed=pages", "notindexed=isbn"
+        )
+        assertOptions(
+            "CREATE VIRTUAL TABLE `Book` USING FTS4(tokenize=porter, " +
+                "`content`, `pages`, notindexed=pages)",
+            "tokenize=porter", "notindexed=pages"
+        )
+        assertOptions(
+            "CREATE VIRTUAL TABLE `Book` USING FTS4(tokenize=porter, " +
+                "`content`, `pages`, notindexed=pages)",
+            "tokenize=porter", "notindexed=pages"
+        )
+        assertOptions(
+            "CREATE VIRTUAL TABLE `Book` USING FTS4(tokenize=unicode61 \"tokenchars=,\")",
+            "tokenize=unicode61 \"tokenchars=,\""
+        )
+        assertOptions(
+            "CREATE VIRTUAL TABLE `Book` USING FTS4(tokenize=unicode61 `tokenchars=,`)",
+            "tokenize=unicode61 `tokenchars=,`"
+        )
+        assertOptions(
+            "CREATE VIRTUAL TABLE `Book` USING FTS4(tokenize=unicode61 " +
+                "\"tokenchars=.=\" \"separators=X\", `pages`, notindexed=pages)",
+            "tokenize=unicode61 \"tokenchars=.=\" \"separators=X\"",
+            "notindexed=pages"
+        )
+        assertOptions(
+            "CREATE VIRTUAL TABLE `Book` USING FTS4(tokenize=porter, " +
+                "`author`, languageid=`lid`, matchinfo=fts3, notindexed=`pages`, " +
+                "order=desc, prefix=`2,4`)",
+            "tokenize=porter", "languageid=`lid`", "matchinfo=fts3",
+            "notindexed=`pages`", "order=desc", "prefix=`2,4`"
+        )
+    }
+
+    private fun assertOptions(createSql: String, vararg options: String) {
+        val actualOptions = parseFtsOptions(createSql)
+        val expectedOptions = Sets.newSet(*options)
+        MatcherAssert.assertThat(actualOptions, CoreMatchers.`is`(expectedOptions))
+    }
+}
diff --git a/room/room-runtime/src/commonMain/kotlin/androidx/room/InvalidationTracker.kt b/room/room-runtime/src/commonMain/kotlin/androidx/room/InvalidationTracker.kt
new file mode 100644
index 0000000..87f71e6
--- /dev/null
+++ b/room/room-runtime/src/commonMain/kotlin/androidx/room/InvalidationTracker.kt
@@ -0,0 +1,39 @@
+/*
+ * Copyright 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.room
+
+import androidx.annotation.RestrictTo
+import androidx.sqlite.SQLiteConnection
+import kotlin.jvm.JvmSuppressWildcards
+
+/**
+ * The invalidation tracker keeps track of modified tables by queries and notifies its registered
+ * [Observer]s about such modifications.
+ */
+expect class InvalidationTracker
+@RestrictTo(RestrictTo.Scope.LIBRARY_GROUP_PREFIX)
+constructor(
+    database: RoomDatabase,
+    shadowTablesMap: Map<String, String>,
+    viewTables: Map<String, @JvmSuppressWildcards Set<String>>,
+    vararg tableNames: String
+) {
+    /**
+     * Internal method to initialize table tracking. Invoked by generated code.
+     */
+    internal fun internalInit(connection: SQLiteConnection)
+}
diff --git a/room/room-runtime/src/commonMain/kotlin/androidx/room/RoomDatabase.kt b/room/room-runtime/src/commonMain/kotlin/androidx/room/RoomDatabase.kt
index 539c5bf..13ba545 100644
--- a/room/room-runtime/src/commonMain/kotlin/androidx/room/RoomDatabase.kt
+++ b/room/room-runtime/src/commonMain/kotlin/androidx/room/RoomDatabase.kt
@@ -24,7 +24,9 @@
 import androidx.room.migration.Migration
 import androidx.room.util.contains
 import androidx.room.util.isAssignableFrom
+import androidx.sqlite.SQLiteConnection
 import androidx.sqlite.SQLiteDriver
+import androidx.sqlite.SQLiteStatement
 import kotlin.jvm.JvmName
 import kotlin.reflect.KClass
 
@@ -40,6 +42,16 @@
 expect abstract class RoomDatabase {
 
     /**
+     * The invalidation tracker for this database.
+     *
+     * You can use the invalidation tracker to get notified when certain tables in the database
+     * are modified.
+     *
+     * @return The invalidation tracker for the database.
+     */
+    val invalidationTracker: InvalidationTracker
+
+    /**
      * Creates a connection manager to manage database connection. Note that this method
      * is called when the [RoomDatabase] is initialized.
      *
@@ -62,6 +74,16 @@
     protected open fun createOpenDelegate(): RoomOpenDelegateMarker
 
     /**
+     * Creates the invalidation tracker
+     *
+     * An implementation of this function is generated by the Room processor. Note that this method
+     * is called when the [RoomDatabase] is initialized.
+     *
+     * @return A new invalidation tracker.
+     */
+    protected abstract fun createInvalidationTracker(): InvalidationTracker
+
+    /**
      * Returns a Set of required [AutoMigrationSpec] classes.
      *
      * An implementation of this function is generated by the Room processor. Note that this method
@@ -126,6 +148,15 @@
     internal val requiredTypeConverterClasses: Map<KClass<*>, List<KClass<*>>>
 
     /**
+     * Initialize invalidation tracker. Note that this method is called when the [RoomDatabase] is
+     * initialized and opens a database connection.
+     *
+     * @param connection The database connection.
+     */
+    @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP_PREFIX)
+    protected fun internalInitInvalidationTracker(connection: SQLiteConnection)
+
+    /**
      * Closes the database.
      *
      * Once a [RoomDatabase] is closed it should no longer be used.
@@ -133,6 +164,21 @@
     fun close()
 
     /**
+     * Performs a database operation.
+     */
+    @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
+    suspend fun <R> perform(isReadOnly: Boolean, sql: String, block: (SQLiteStatement) -> R): R
+
+    /**
+     * Performs a database transaction operation.
+     */
+    @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
+    suspend fun <R> performTransaction(
+        isReadOnly: Boolean,
+        block: suspend (TransactionScope<R>) -> R
+    ): R
+
+    /**
      * Journal modes for SQLite database.
      *
      * @see Builder.setJournalMode
diff --git a/room/room-runtime/src/commonMain/kotlin/androidx/room/util/FtsTableInfo.kt b/room/room-runtime/src/commonMain/kotlin/androidx/room/util/FtsTableInfo.kt
new file mode 100644
index 0000000..05b487d
--- /dev/null
+++ b/room/room-runtime/src/commonMain/kotlin/androidx/room/util/FtsTableInfo.kt
@@ -0,0 +1,82 @@
+/*
+ * Copyright 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.room.util
+
+import androidx.annotation.RestrictTo
+import androidx.sqlite.SQLiteConnection
+import kotlin.jvm.JvmField
+import kotlin.jvm.JvmStatic
+
+/**
+ * A data class that holds the information about an FTS table.
+ *
+ */
+@RestrictTo(RestrictTo.Scope.LIBRARY_GROUP_PREFIX)
+expect class FtsTableInfo {
+    /**
+     * The table name
+     */
+    @JvmField
+    val name: String
+
+    /**
+     * The column names
+     */
+    @JvmField
+    val columns: Set<String>
+
+    /**
+     * The set of options. Each value in the set contains the option in the following format:
+     * <key, value>.
+     */
+    @JvmField
+    val options: Set<String>
+
+    constructor(name: String, columns: Set<String>, createSql: String)
+
+    companion object {
+        /**
+         * Reads the table information from the given database.
+         *
+         * @param connection The database connection to read the information from.
+         * @param tableName The table name.
+         * @return A FtsTableInfo containing the columns and options for the provided table name.
+         */
+        @JvmStatic
+        fun read(connection: SQLiteConnection, tableName: String): FtsTableInfo
+    }
+}
+
+internal fun FtsTableInfo.equalsCommon(other: Any?): Boolean {
+    if (this === other) return true
+    if (other !is FtsTableInfo) return false
+    val that = other
+    if (name != that.name) return false
+    if (columns != that.columns) return false
+    return options == that.options
+}
+
+internal fun FtsTableInfo.hashCodeCommon(): Int {
+    var result = name.hashCode()
+    result = 31 * result + (columns.hashCode())
+    result = 31 * result + (options.hashCode())
+    return result
+}
+
+internal fun FtsTableInfo.toStringCommon(): String {
+    return ("FtsTableInfo{name='$name', columns=$columns, options=$options'}")
+}
diff --git a/room/room-runtime/src/commonMain/kotlin/androidx/room/util/SchemaInfoUtil.kt b/room/room-runtime/src/commonMain/kotlin/androidx/room/util/SchemaInfoUtil.kt
new file mode 100644
index 0000000..d4ebdd5
--- /dev/null
+++ b/room/room-runtime/src/commonMain/kotlin/androidx/room/util/SchemaInfoUtil.kt
@@ -0,0 +1,367 @@
+/*
+ * Copyright 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.room.util
+
+import androidx.room.ColumnInfo
+import androidx.sqlite.SQLiteConnection
+import androidx.sqlite.SQLiteStatement
+import androidx.sqlite.use
+
+/**
+ * Implements https://www.sqlite.org/datatype3.html section 3.1
+ *
+ * @param type The type that was given to the sqlite
+ * @return The normalized type which is one of the 5 known affinities
+ */
+@ColumnInfo.SQLiteTypeAffinity
+internal fun findAffinity(type: String?): Int {
+    if (type == null) {
+        return ColumnInfo.BLOB
+    }
+    val uppercaseType = type.uppercase()
+    if (uppercaseType.contains("INT")) {
+        return ColumnInfo.INTEGER
+    }
+    if (uppercaseType.contains("CHAR") ||
+        uppercaseType.contains("CLOB") ||
+        uppercaseType.contains("TEXT")
+    ) {
+        return ColumnInfo.TEXT
+    }
+    if (uppercaseType.contains("BLOB")) {
+        return ColumnInfo.BLOB
+    }
+    if (uppercaseType.contains("REAL") ||
+        uppercaseType.contains("FLOA") ||
+        uppercaseType.contains("DOUB")
+    ) {
+        return ColumnInfo.REAL
+    }
+    // SQLite returns NUMERIC here but it is like a catch all. We already
+    // have UNDEFINED so it is better to use UNDEFINED for consistency.
+    return ColumnInfo.UNDEFINED
+}
+
+internal fun readTableInfo(connection: SQLiteConnection, tableName: String): TableInfo {
+    val columns = readColumns(connection, tableName)
+    val foreignKeys = readForeignKeys(connection, tableName)
+    val indices = readIndices(connection, tableName)
+    return TableInfo(tableName, columns, foreignKeys, indices)
+}
+
+private fun readForeignKeys(
+    connection: SQLiteConnection,
+    tableName: String
+): Set<TableInfo.ForeignKey> {
+    // this seems to return everything in order but it is not documented so better be safe
+    connection.prepare("PRAGMA foreign_key_list(`$tableName`)").use { stmt ->
+        val idColumnIndex = stmt.getColumnIndex("id")
+        val seqColumnIndex = stmt.getColumnIndex("seq")
+        val tableColumnIndex = stmt.getColumnIndex("table")
+        val onDeleteColumnIndex = stmt.getColumnIndex("on_delete")
+        val onUpdateColumnIndex = stmt.getColumnIndex("on_update")
+        val ordered = readForeignKeyFieldMappings(stmt)
+
+        // Reset cursor as readForeignKeyFieldMappings has moved it
+        stmt.reset()
+        return buildSet {
+            while (stmt.step()) {
+                val seq = stmt.getLong(seqColumnIndex)
+                if (seq != 0L) {
+                    continue
+                }
+                val id = stmt.getLong(idColumnIndex).toInt()
+                val myColumns = mutableListOf<String>()
+                val refColumns = mutableListOf<String>()
+
+                ordered.filter {
+                    it.id == id
+                }.forEach { key ->
+                    myColumns.add(key.from)
+                    refColumns.add(key.to)
+                }
+
+                add(
+                    TableInfo.ForeignKey(
+                        referenceTable = stmt.getText(tableColumnIndex),
+                        onDelete = stmt.getText(onDeleteColumnIndex),
+                        onUpdate = stmt.getText(onUpdateColumnIndex),
+                        columnNames = myColumns,
+                        referenceColumnNames = refColumns
+                    )
+                )
+            }
+        }
+    }
+}
+
+/**
+ * Temporary data holder for a foreign key row in the pragma result. We need this to ensure
+ * sorting in the generated foreign key object.
+ */
+private class ForeignKeyWithSequence(
+    val id: Int,
+    val sequence: Int,
+    val from: String,
+    val to: String
+) : Comparable<ForeignKeyWithSequence> {
+    override fun compareTo(other: ForeignKeyWithSequence): Int {
+        val idCmp = id - other.id
+        return if (idCmp == 0) {
+            sequence - other.sequence
+        } else {
+            idCmp
+        }
+    }
+}
+
+private fun readForeignKeyFieldMappings(
+    stmt: SQLiteStatement
+): List<ForeignKeyWithSequence> {
+    val idColumnIndex = stmt.getColumnIndex("id")
+    val seqColumnIndex = stmt.getColumnIndex("seq")
+    val fromColumnIndex = stmt.getColumnIndex("from")
+    val toColumnIndex = stmt.getColumnIndex("to")
+
+    return buildList {
+        while (stmt.step()) {
+            add(
+                ForeignKeyWithSequence(
+                    id = stmt.getLong(idColumnIndex).toInt(),
+                    sequence = stmt.getLong(seqColumnIndex).toInt(),
+                    from = stmt.getText(fromColumnIndex),
+                    to = stmt.getText(toColumnIndex)
+                )
+            )
+        }
+    }.sorted()
+}
+
+private fun readColumns(
+    connection: SQLiteConnection,
+    tableName: String
+): Map<String, TableInfo.Column> {
+    connection.prepare("PRAGMA table_info(`$tableName`)").use { stmt ->
+        if (!stmt.step()) {
+            return emptyMap()
+        }
+
+        val nameIndex = stmt.getColumnIndex("name")
+        val typeIndex = stmt.getColumnIndex("type")
+        val notNullIndex = stmt.getColumnIndex("notnull")
+        val pkIndex = stmt.getColumnIndex("pk")
+        val defaultValueIndex = stmt.getColumnIndex("dflt_value")
+
+        return buildMap {
+            do {
+                val name = stmt.getText(nameIndex)
+                val type = stmt.getText(typeIndex)
+                val notNull = stmt.getLong(notNullIndex) != 0L
+                val primaryKeyPosition = stmt.getLong(pkIndex).toInt()
+                val defaultValue =
+                    if (stmt.isNull(defaultValueIndex)) null else stmt.getText(defaultValueIndex)
+                put(
+                    key = name,
+                    value = TableInfo.Column(
+                        name = name,
+                        type = type,
+                        notNull = notNull,
+                        primaryKeyPosition = primaryKeyPosition,
+                        defaultValue = defaultValue,
+                        createdFrom = TableInfo.CREATED_FROM_DATABASE
+                    )
+                )
+            } while (stmt.step())
+        }
+    }
+}
+
+/**
+ * @return null if we cannot read the indices due to older sqlite implementations.
+ */
+private fun readIndices(connection: SQLiteConnection, tableName: String): Set<TableInfo.Index>? {
+    connection.prepare("PRAGMA index_list(`$tableName`)").use { stmt ->
+        val nameColumnIndex = stmt.getColumnIndex("name")
+        val originColumnIndex = stmt.getColumnIndex("origin")
+        val uniqueIndex = stmt.getColumnIndex("unique")
+        if (nameColumnIndex == -1 || originColumnIndex == -1 || uniqueIndex == -1) {
+            // we cannot read them so better not validate any index.
+            return null
+        }
+        return buildSet {
+            while (stmt.step()) {
+                val origin = stmt.getText(originColumnIndex)
+                if ("c" != origin) {
+                    // Ignore auto-created indices
+                    continue
+                }
+                val name = stmt.getText(nameColumnIndex)
+                val unique = stmt.getLong(uniqueIndex) == 1L
+                // Read index but if we cannot read it properly so better not read it
+                val index = readIndex(connection, name, unique) ?: return null
+                add(index)
+            }
+        }
+    }
+}
+
+/**
+ * @return null if we cannot read the index due to older sqlite implementations.
+ */
+private fun readIndex(
+    connection: SQLiteConnection,
+    name: String,
+    unique: Boolean
+): TableInfo.Index? {
+    return connection.prepare("PRAGMA index_xinfo(`$name`)").use { stmt ->
+        val seqnoColumnIndex = stmt.getColumnIndex("seqno")
+        val cidColumnIndex = stmt.getColumnIndex("cid")
+        val nameColumnIndex = stmt.getColumnIndex("name")
+        val descColumnIndex = stmt.getColumnIndex("desc")
+        if (
+            seqnoColumnIndex == -1 ||
+            cidColumnIndex == -1 ||
+            nameColumnIndex == -1 ||
+            descColumnIndex == -1
+        ) {
+            // we cannot read them so better not validate any index.
+            return null
+        }
+        val columnsMap = mutableMapOf<Int, String>()
+        val ordersMap = mutableMapOf<Int, String>()
+        while (stmt.step()) {
+            val cid = stmt.getLong(cidColumnIndex).toInt()
+            if (cid < 0) {
+                // Ignore SQLite row ID
+                continue
+            }
+            val seq = stmt.getLong(seqnoColumnIndex).toInt()
+            val columnName = stmt.getText(nameColumnIndex)
+            val order = if (stmt.getLong(descColumnIndex) > 0) "DESC" else "ASC"
+            columnsMap[seq] = columnName
+            ordersMap[seq] = order
+        }
+        val columns = columnsMap.entries.sortedBy { it.key }.map { it.value }.toList()
+        val orders = ordersMap.entries.sortedBy { it.key }.map { it.value }.toList()
+        TableInfo.Index(name, unique, columns, orders)
+    }
+}
+
+internal fun readFtsColumns(connection: SQLiteConnection, tableName: String): Set<String> {
+    return buildSet {
+        connection.prepare("PRAGMA table_info(`$tableName`)").use { stmt ->
+            if (!stmt.step()) return@use
+            val nameIndex = stmt.getColumnIndex("name")
+            do {
+                add(stmt.getText(nameIndex))
+            } while (stmt.step())
+        }
+    }
+}
+
+internal fun readFtsOptions(connection: SQLiteConnection, tableName: String): Set<String> {
+    val sql = connection.prepare(
+        "SELECT * FROM sqlite_master WHERE `name` = '$tableName'"
+    ).use { stmt ->
+        if (stmt.step()) {
+            stmt.getText(stmt.getColumnIndex("sql"))
+        } else {
+            ""
+        }
+    }
+    return parseFtsOptions(sql)
+}
+
+// A set of valid FTS Options
+private val FTS_OPTIONS = arrayOf(
+    "tokenize=", "compress=", "content=", "languageid=", "matchinfo=", "notindexed=",
+    "order=", "prefix=", "uncompress="
+)
+
+/**
+ * Parses FTS options from the create statement of an FTS table.
+ *
+ * This method assumes the given create statement is a valid well-formed SQLite statement as
+ * defined in the [CREATE VIRTUAL TABLE
+         * syntax diagram](https://www.sqlite.org/lang_createvtab.html).
+ *
+ * @param createStatement the "CREATE VIRTUAL TABLE" statement.
+ * @return the set of FTS option key and values in the create statement.
+ */
+internal fun parseFtsOptions(createStatement: String): Set<String> {
+    if (createStatement.isEmpty()) {
+        return emptySet()
+    }
+
+    // Module arguments are within the parenthesis followed by the module name.
+    val argsString = createStatement.substring(
+        createStatement.indexOf('(') + 1,
+        createStatement.lastIndexOf(')')
+    )
+
+    // Split the module argument string by the comma delimiter, keeping track of quotation
+    // so that if the delimiter is found within a string literal we don't substring at the
+    // wrong index. SQLite supports four ways of quoting keywords, see:
+    // https://www.sqlite.org/lang_keywords.html
+    val args = mutableListOf<String>()
+    val quoteStack = ArrayDeque<Char>()
+    var lastDelimiterIndex = -1
+    argsString.forEachIndexed { i, value ->
+        when (value) {
+            '\'', '"', '`' ->
+                if (quoteStack.isEmpty()) {
+                    quoteStack.addFirst(value)
+                } else if (quoteStack.firstOrNull() == value) {
+                    quoteStack.removeLast()
+                }
+            '[' -> if (quoteStack.isEmpty()) {
+                quoteStack.addFirst(value)
+            }
+            ']' -> if (!quoteStack.isEmpty() && quoteStack.firstOrNull() == '[') {
+                quoteStack.removeLast()
+            }
+            ',' -> if (quoteStack.isEmpty()) {
+                args.add(argsString.substring(lastDelimiterIndex + 1, i).trim { it <= ' ' })
+                lastDelimiterIndex = i
+            }
+        }
+    }
+
+    // Add final argument.
+    args.add(argsString.substring(lastDelimiterIndex + 1).trim())
+
+    // Match args against valid options, otherwise they are column definitions.
+    val options = args.filter { arg ->
+        FTS_OPTIONS.any { validOption ->
+            arg.startsWith(validOption)
+        }
+    }.toSet()
+    return options
+}
+
+internal fun readViewInfo(connection: SQLiteConnection, viewName: String): ViewInfo {
+    return connection.prepare(
+        "SELECT name, sql FROM sqlite_master " +
+            "WHERE type = 'view' AND name = '$viewName'"
+    ).use { stmt ->
+        if (stmt.step()) {
+            ViewInfo(stmt.getText(0), stmt.getText(1))
+        } else {
+            ViewInfo(viewName, null)
+        }
+    }
+}
diff --git a/room/room-runtime/src/commonMain/kotlin/androidx/room/util/StatementUtil.kt b/room/room-runtime/src/commonMain/kotlin/androidx/room/util/StatementUtil.kt
new file mode 100644
index 0000000..cebd289
--- /dev/null
+++ b/room/room-runtime/src/commonMain/kotlin/androidx/room/util/StatementUtil.kt
@@ -0,0 +1,55 @@
+/*
+ * Copyright 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+@file:JvmMultifileClass
+@file:JvmName("SQLiteStatementUtil")
+
+package androidx.room.util
+
+import androidx.annotation.RestrictTo
+import androidx.sqlite.SQLiteStatement
+import kotlin.jvm.JvmMultifileClass
+import kotlin.jvm.JvmName
+
+/**
+ * Returns the zero-based index for the given column name, or throws [IllegalArgumentException] if
+ * the column doesn't exist.
+ */
+@RestrictTo(RestrictTo.Scope.LIBRARY_GROUP_PREFIX)
+fun getColumnIndexOrThrow(stmt: SQLiteStatement, name: String): Int {
+    val index: Int = stmt.getColumnIndex(name)
+    if (index >= 0) {
+        return index
+    }
+    val availableColumns = List(stmt.getColumnCount()) { stmt.getColumnName(it) }.joinToString()
+    throw IllegalArgumentException(
+        "Column '$name' does not exist. Available columns: [$availableColumns]"
+    )
+}
+
+/**
+ * Returns the zero-based index for the given column name, or -1 if the column doesn't exist.
+ */
+internal expect fun SQLiteStatement.getColumnIndex(name: String): Int
+
+// TODO(b/322183292): Consider optimizing by creating a String->Int map, similar to Android
+internal fun SQLiteStatement.columnIndexOf(name: String): Int {
+    val columnCount = getColumnCount()
+    for (i in 0 until columnCount) {
+        if (name == getColumnName(i)) return i
+    }
+    return -1
+}
diff --git a/room/room-runtime/src/commonMain/kotlin/androidx/room/util/TableInfo.kt b/room/room-runtime/src/commonMain/kotlin/androidx/room/util/TableInfo.kt
new file mode 100644
index 0000000..891b8e7
--- /dev/null
+++ b/room/room-runtime/src/commonMain/kotlin/androidx/room/util/TableInfo.kt
@@ -0,0 +1,397 @@
+/*
+ * Copyright (C) 2017 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR 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.util
+
+import androidx.annotation.RestrictTo
+import androidx.room.ColumnInfo.SQLiteTypeAffinity
+import androidx.sqlite.SQLiteConnection
+import kotlin.jvm.JvmField
+import kotlin.jvm.JvmStatic
+
+/**
+ * A data class that holds the information about a table.
+ *
+ * It directly maps to the result of `PRAGMA table_info(<table_name>)`. Check the
+ * [PRAGMA table_info](http://www.sqlite.org/pragma.html#pragma_table_info)
+ * documentation for more details.
+ *
+ * Even though SQLite column names are case insensitive, this class uses case sensitive matching.
+ */
+@RestrictTo(RestrictTo.Scope.LIBRARY_GROUP_PREFIX)
+expect class TableInfo(
+    name: String,
+    columns: Map<String, Column>,
+    foreignKeys: Set<ForeignKey>,
+    indices: Set<Index>? = null
+) {
+    /**
+     * The table name.
+     */
+    @JvmField
+    val name: String
+    @JvmField
+    val columns: Map<String, Column>
+    @JvmField
+    val foreignKeys: Set<ForeignKey>
+    @JvmField
+    val indices: Set<Index>?
+
+    override fun equals(other: Any?): Boolean
+
+    override fun hashCode(): Int
+
+    override fun toString(): String
+
+    companion object {
+        /**
+         * Identifier for when the info is created from an unknown source.
+         */
+        val CREATED_FROM_UNKNOWN: Int
+
+        /**
+         * Identifier for when the info is created from an entity definition, such as generated code
+         * by the compiler or at runtime from a schema bundle, parsed from a schema JSON file.
+         */
+        val CREATED_FROM_ENTITY: Int
+
+        /**
+         * Identifier for when the info is created from the database itself, reading information
+         * from a PRAGMA, such as table_info.
+         */
+        val CREATED_FROM_DATABASE: Int
+
+        /**
+         * Reads the table information from the given database.
+         *
+         * @param connection The database connection to read the information from.
+         * @param tableName The table name.
+         * @return A TableInfo containing the schema information for the provided table name.
+         */
+        @JvmStatic
+        fun read(connection: SQLiteConnection, tableName: String): TableInfo
+    }
+
+    /**
+     * Holds the information about a database column.
+     */
+    @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP_PREFIX)
+    class Column(
+        name: String,
+        type: String,
+        notNull: Boolean,
+        primaryKeyPosition: Int,
+        defaultValue: String?,
+        createdFrom: Int
+    ) {
+        /**
+         * The column name.
+         */
+        @JvmField
+        val name: String
+        /**
+         * The column type affinity.
+         */
+        @JvmField
+        val type: String
+        /**
+         * Whether or not the column can be NULL.
+         */
+        @JvmField
+        val notNull: Boolean
+        @JvmField
+        val primaryKeyPosition: Int
+        @JvmField
+        val defaultValue: String?
+        @JvmField
+        val createdFrom: Int
+
+        /**
+         * The column type after it is normalized to one of the basic types according to
+         * https://www.sqlite.org/datatype3.html Section 3.1.
+         *
+         * This is the value Room uses for equality check.
+         */
+        @SQLiteTypeAffinity
+        @JvmField
+        val affinity: Int
+
+        /**
+         * Returns whether this column is part of the primary key or not.
+         *
+         * @return True if this column is part of the primary key, false otherwise.
+         */
+        val isPrimaryKey: Boolean
+
+        override fun equals(other: Any?): Boolean
+
+        override fun hashCode(): Int
+
+        override fun toString(): String
+    }
+
+    /**
+     * Holds the information about an SQLite foreign key
+     *
+     */
+    @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP_PREFIX)
+    class ForeignKey(
+        referenceTable: String,
+        onDelete: String,
+        onUpdate: String,
+        columnNames: List<String>,
+        referenceColumnNames: List<String>
+    ) {
+        @JvmField
+        val referenceTable: String
+        @JvmField
+        val onDelete: String
+        @JvmField
+        val onUpdate: String
+        @JvmField
+        val columnNames: List<String>
+        @JvmField
+        val referenceColumnNames: List<String>
+
+        override fun equals(other: Any?): Boolean
+
+        override fun hashCode(): Int
+
+        override fun toString(): String
+    }
+
+    /**
+     * Holds the information about an SQLite index
+     *
+     */
+    @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP_PREFIX)
+    class Index(
+        name: String,
+        unique: Boolean,
+        columns: List<String>,
+        orders: List<String>
+    ) {
+
+        @JvmField
+        val name: String
+        @JvmField
+        val unique: Boolean
+        @JvmField
+        val columns: List<String>
+        @JvmField
+        var orders: List<String>
+
+        companion object {
+            // should match the value in Index.kt
+            val DEFAULT_PREFIX: String
+        }
+
+        override fun equals(other: Any?): Boolean
+
+        override fun hashCode(): Int
+
+        override fun toString(): String
+    }
+}
+
+internal fun TableInfo.equalsCommon(other: Any?): Boolean {
+    if (this === other) return true
+    if (other !is TableInfo) return false
+    if (name != other.name) return false
+    if (columns != other.columns) {
+        return false
+    }
+    if (foreignKeys != other.foreignKeys) {
+        return false
+    }
+    return if (indices == null || other.indices == null) {
+        // if one us is missing index information, seems like we couldn't acquire the
+        // information so we better skip.
+        true
+    } else indices == other.indices
+}
+
+internal fun TableInfo.hashCodeCommon(): Int {
+    var result = name.hashCode()
+    result = 31 * result + columns.hashCode()
+    result = 31 * result + foreignKeys.hashCode()
+    // skip index, it is not reliable for comparison.
+    return result
+}
+
+internal fun TableInfo.toStringCommon(): String {
+    return ("TableInfo{name='$name', columns=$columns, foreignKeys=$foreignKeys, " +
+        "indices=$indices}")
+}
+
+internal fun TableInfo.Column.equalsCommon(other: Any?): Boolean {
+    if (this === other) return true
+    if (other !is TableInfo.Column) return false
+    if (!equalsInPrimaryKey(other)) return false
+    if (name != other.name) return false
+    if (notNull != other.notNull) return false
+    // Only validate default value if it was defined in an entity, i.e. if the info
+    // from the compiler itself has it. b/136019383
+    if (
+        createdFrom == TableInfo.CREATED_FROM_ENTITY &&
+        other.createdFrom == TableInfo.CREATED_FROM_DATABASE &&
+        defaultValue != null &&
+        !defaultValueEqualsCommon(defaultValue, other.defaultValue)
+    ) {
+        return false
+    } else if (
+        createdFrom == TableInfo.CREATED_FROM_DATABASE &&
+        other.createdFrom == TableInfo.CREATED_FROM_ENTITY &&
+        other.defaultValue != null &&
+        !defaultValueEqualsCommon(other.defaultValue, defaultValue)
+    ) {
+        return false
+    } else if (
+        createdFrom != TableInfo.CREATED_FROM_UNKNOWN &&
+        createdFrom == other.createdFrom &&
+        (if (defaultValue != null)
+            !defaultValueEqualsCommon(defaultValue, other.defaultValue)
+        else other.defaultValue != null)
+    ) {
+        return false
+    }
+    return affinity == other.affinity
+}
+
+/**
+ * Checks if the primary key match.
+ */
+internal expect fun TableInfo.Column.equalsInPrimaryKey(other: TableInfo.Column): Boolean
+
+/**
+ * Checks if the default values provided match. Handles the special case in which the
+ * default value is surrounded by parenthesis (e.g. encountered in b/182284899).
+ *
+ * Surrounding parenthesis are removed by SQLite when reading from the database, hence
+ * this function will check if they are present in the actual value, if so, it will
+ * compare the two values by ignoring the surrounding parenthesis.
+ *
+ */
+internal fun defaultValueEqualsCommon(current: String, other: String?): Boolean {
+    if (current == other) {
+        return true
+    } else if (containsSurroundingParenthesis(current)) {
+        return current.substring(1, current.length - 1).trim() == other
+    }
+    return false
+}
+
+/**
+ * Checks for potential surrounding parenthesis, if found, removes them and checks if
+ * remaining parenthesis are balanced. If so, the surrounding parenthesis are redundant,
+ * and returns true.
+ */
+private fun containsSurroundingParenthesis(current: String): Boolean {
+    if (current.isEmpty()) {
+        return false
+    }
+    var surroundingParenthesis = 0
+    current.forEachIndexed { i, c ->
+        if (i == 0 && c != '(') {
+            return false
+        }
+        if (c == '(') {
+            surroundingParenthesis++
+        } else if (c == ')') {
+            surroundingParenthesis--
+            if (surroundingParenthesis == 0 && i != current.length - 1) {
+                return false
+            }
+        }
+    }
+    return surroundingParenthesis == 0
+}
+
+internal fun TableInfo.Column.hashCodeCommon(): Int {
+    var result = name.hashCode()
+    result = 31 * result + affinity
+    result = 31 * result + if (notNull) 1231 else 1237
+    result = 31 * result + primaryKeyPosition
+    // Default value is not part of the hashcode since we conditionally check it for
+    // equality which would break the equals + hashcode contract.
+    // result = 31 * result + (defaultValue != null ? defaultValue.hashCode() : 0);
+    return result
+}
+
+internal fun TableInfo.Column.toStringCommon(): String {
+    return "Column{name='$name', type='$type', affinity='$affinity', " +
+        "notNull=$notNull, primaryKeyPosition=$primaryKeyPosition, " +
+        "defaultValue='${defaultValue ?: "undefined"}'}"
+}
+
+internal fun TableInfo.ForeignKey.equalsCommon(other: Any?): Boolean {
+    if (this === other) return true
+    if (other !is TableInfo.ForeignKey) return false
+    if (referenceTable != other.referenceTable) return false
+    if (onDelete != other.onDelete) return false
+    if (onUpdate != other.onUpdate) return false
+    return if (columnNames != other.columnNames) false else referenceColumnNames ==
+        other.referenceColumnNames
+}
+
+internal fun TableInfo.ForeignKey.hashCodeCommon(): Int {
+    var result = referenceTable.hashCode()
+    result = 31 * result + onDelete.hashCode()
+    result = 31 * result + onUpdate.hashCode()
+    result = 31 * result + columnNames.hashCode()
+    result = 31 * result + referenceColumnNames.hashCode()
+    return result
+}
+
+internal fun TableInfo.ForeignKey.toStringCommon(): String {
+    return "ForeignKey{referenceTable='$referenceTable', onDelete='$onDelete +', " +
+        "onUpdate='$onUpdate', columnNames=$columnNames, " +
+        "referenceColumnNames=$referenceColumnNames}"
+}
+
+internal fun TableInfo.Index.equalsCommon(other: Any?): Boolean {
+    if (this === other) return true
+    if (other !is TableInfo.Index) return false
+    if (unique != other.unique) {
+        return false
+    }
+    if (columns != other.columns) {
+        return false
+    }
+    if (orders != other.orders) {
+        return false
+    }
+    return if (name.startsWith(TableInfo.Index.DEFAULT_PREFIX)) {
+        other.name.startsWith(TableInfo.Index.DEFAULT_PREFIX)
+    } else {
+        name == other.name
+    }
+}
+
+internal fun TableInfo.Index.hashCodeCommon(): Int {
+    var result = if (name.startsWith(TableInfo.Index.DEFAULT_PREFIX)) {
+        TableInfo.Index.DEFAULT_PREFIX.hashCode()
+    } else {
+        name.hashCode()
+    }
+    result = 31 * result + if (unique) 1 else 0
+    result = 31 * result + columns.hashCode()
+    result = 31 * result + orders.hashCode()
+    return result
+}
+
+internal fun TableInfo.Index.toStringCommon(): String {
+    return "Index{name='$name', unique=$unique, columns=$columns, orders=$orders'}"
+}
diff --git a/room/room-runtime/src/commonMain/kotlin/androidx/room/util/ViewInfo.kt b/room/room-runtime/src/commonMain/kotlin/androidx/room/util/ViewInfo.kt
new file mode 100644
index 0000000..8720f4a
--- /dev/null
+++ b/room/room-runtime/src/commonMain/kotlin/androidx/room/util/ViewInfo.kt
@@ -0,0 +1,79 @@
+/*
+ * Copyright 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package androidx.room.util
+
+import androidx.annotation.RestrictTo
+import androidx.sqlite.SQLiteConnection
+import kotlin.jvm.JvmField
+import kotlin.jvm.JvmStatic
+
+/**
+ * A data class that holds the information about a view.
+ *
+ * This derives information from sqlite_master.
+ *
+ * Even though SQLite column names are case insensitive, this class uses case sensitive matching.
+ */
+@RestrictTo(RestrictTo.Scope.LIBRARY_GROUP_PREFIX)
+expect class ViewInfo(
+    name: String,
+    sql: String?
+) {
+    /**
+     * The view name
+     */
+    @JvmField
+    val name: String
+    /**
+     * The SQL of CREATE VIEW.
+     */
+    @JvmField
+    val sql: String?
+
+    override fun equals(other: Any?): Boolean
+
+    override fun hashCode(): Int
+
+    override fun toString(): String
+
+    companion object {
+        /**
+         * Reads the view information from the given database.
+         *
+         * @param connection The database connection to read the information from.
+         * @param viewName The view name.
+         * @return A ViewInfo containing the schema information for the provided view name.
+         */
+        @JvmStatic
+        fun read(connection: SQLiteConnection, viewName: String): ViewInfo
+    }
+}
+
+internal fun ViewInfo.equalsCommon(other: Any?): Boolean {
+    if (this === other) return true
+    if (other !is ViewInfo) return false
+    return ((name == other.name) && if (sql != null) sql == other.sql else other.sql == null)
+}
+
+internal fun ViewInfo.hashCodeCommon(): Int {
+    var result = name.hashCode()
+    result = 31 * result + (sql?.hashCode() ?: 0)
+    return result
+}
+
+internal fun ViewInfo.toStringCommon(): String {
+    return "ViewInfo{name='$name', sql='$sql'}"
+}
diff --git a/room/room-runtime/src/jvmMain/kotlin/androidx/room/InvalidationTracker.jvm.kt b/room/room-runtime/src/jvmMain/kotlin/androidx/room/InvalidationTracker.jvm.kt
new file mode 100644
index 0000000..98e4dfe
--- /dev/null
+++ b/room/room-runtime/src/jvmMain/kotlin/androidx/room/InvalidationTracker.jvm.kt
@@ -0,0 +1,39 @@
+/*
+ * Copyright 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.room
+
+import androidx.annotation.RestrictTo
+import androidx.sqlite.SQLiteConnection
+
+/**
+ * The invalidation tracker keeps track of modified tables by queries and notifies its registered
+ * [Observer]s about such modifications.
+ */
+actual class InvalidationTracker
+@RestrictTo(RestrictTo.Scope.LIBRARY_GROUP_PREFIX)
+actual constructor(
+    database: RoomDatabase,
+    shadowTablesMap: Map<String, String>,
+    viewTables: Map<String, Set<String>>,
+    vararg tableNames: String
+) {
+    /**
+     * Internal method to initialize table tracking. Invoked by generated code.
+     */
+    internal actual fun internalInit(connection: SQLiteConnection) {
+    }
+}
diff --git a/room/room-runtime/src/jvmMain/kotlin/androidx/room/RoomDatabase.jvm.kt b/room/room-runtime/src/jvmMain/kotlin/androidx/room/RoomDatabase.jvm.kt
index 37216e0..3299961 100644
--- a/room/room-runtime/src/jvmMain/kotlin/androidx/room/RoomDatabase.jvm.kt
+++ b/room/room-runtime/src/jvmMain/kotlin/androidx/room/RoomDatabase.jvm.kt
@@ -19,7 +19,9 @@
 import androidx.annotation.RestrictTo
 import androidx.room.migration.AutoMigrationSpec
 import androidx.room.migration.Migration
+import androidx.sqlite.SQLiteConnection
 import androidx.sqlite.SQLiteDriver
+import androidx.sqlite.SQLiteStatement
 import kotlin.reflect.KClass
 
 /**
@@ -38,6 +40,16 @@
     private val typeConverters: MutableMap<KClass<*>, Any> = mutableMapOf()
 
     /**
+     * The invalidation tracker for this database.
+     *
+     * You can use the invalidation tracker to get notified when certain tables in the database
+     * are modified.
+     *
+     * @return The invalidation tracker for the database.
+     */
+    actual val invalidationTracker: InvalidationTracker = createInvalidationTracker()
+
+    /**
      * Called by Room when it is initialized.
      *
      * @param configuration The database configuration.
@@ -78,6 +90,16 @@
     }
 
     /**
+     * Creates the invalidation tracker
+     *
+     * An implementation of this function is generated by the Room processor. Note that this method
+     * is called when the [RoomDatabase] is initialized.
+     *
+     * @return A new invalidation tracker.
+     */
+    protected actual abstract fun createInvalidationTracker(): InvalidationTracker
+
+    /**
      * Returns a Set of required [AutoMigrationSpec] classes.
      *
      * An implementation of this function is generated by the Room processor. Note that this method
@@ -156,6 +178,17 @@
         get() = getRequiredTypeConverterClasses()
 
     /**
+     * Initialize invalidation tracker. Note that this method is called when the [RoomDatabase] is
+     * initialized and opens a database connection.
+     *
+     * @param connection The database connection.
+     */
+    @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP_PREFIX)
+    protected actual fun internalInitInvalidationTracker(connection: SQLiteConnection) {
+        invalidationTracker.internalInit(connection)
+    }
+
+    /**
      * Closes the database.
      *
      * Once a [RoomDatabase] is closed it should no longer be used.
@@ -165,6 +198,39 @@
     }
 
     /**
+     * Performs a database operation.
+     */
+    @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
+    actual suspend fun <R> perform(
+        isReadOnly: Boolean,
+        sql: String,
+        block: (SQLiteStatement) -> R
+    ): R {
+        return connectionManager.useConnection(isReadOnly) { connection ->
+            connection.usePrepared(sql, block)
+        }
+    }
+
+    /**
+     * Performs a database operation in a transaction.
+     */
+    @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
+    actual suspend fun <R> performTransaction(
+        isReadOnly: Boolean,
+        block: suspend (TransactionScope<R>) -> R
+    ): R {
+        return connectionManager.useConnection(isReadOnly) { transactor ->
+            val type = if (isReadOnly) {
+                Transactor.SQLiteTransactionType.DEFERRED
+            } else {
+                Transactor.SQLiteTransactionType.IMMEDIATE
+            }
+            // TODO: Notify Invalidation Tracker before and after transaction block.
+            transactor.withTransaction(type, block)
+        }
+    }
+
+    /**
      * Journal modes for SQLite database.
      *
      * @see Builder.setJournalMode
diff --git a/room/room-runtime/src/jvmMain/kotlin/androidx/room/util/FtsTableInfo.jvm.kt b/room/room-runtime/src/jvmMain/kotlin/androidx/room/util/FtsTableInfo.jvm.kt
new file mode 100644
index 0000000..f2e451d
--- /dev/null
+++ b/room/room-runtime/src/jvmMain/kotlin/androidx/room/util/FtsTableInfo.jvm.kt
@@ -0,0 +1,70 @@
+/*
+ * Copyright 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package androidx.room.util
+
+import androidx.annotation.RestrictTo
+import androidx.sqlite.SQLiteConnection
+
+/**
+ * A data class that holds the information about an FTS table.
+ *
+ */
+@RestrictTo(RestrictTo.Scope.LIBRARY_GROUP_PREFIX)
+actual class FtsTableInfo(
+    /**
+     * The table name
+     */
+    @JvmField
+    actual val name: String,
+
+    /**
+     * The column names
+     */
+    @JvmField
+    actual val columns: Set<String>,
+
+    /**
+     * The set of options. Each value in the set contains the option in the following format:
+     * <key, value>.
+     */
+    @JvmField
+    actual val options: Set<String>
+) {
+    actual constructor(name: String, columns: Set<String>, createSql: String) :
+        this(name, columns, parseFtsOptions(createSql))
+
+    override fun equals(other: Any?) = equalsCommon(other)
+
+    override fun hashCode() = hashCodeCommon()
+
+    override fun toString() = toStringCommon()
+
+    actual companion object {
+        /**
+         * Reads the table information from the given database.
+         *
+         * @param connection The database connection to read the information from.
+         * @param tableName The table name.
+         * @return A FtsTableInfo containing the columns and options for the provided table name.
+         */
+        @JvmStatic
+        actual fun read(connection: SQLiteConnection, tableName: String): FtsTableInfo {
+            val columns = readFtsColumns(connection, tableName)
+            val options = readFtsOptions(connection, tableName)
+            return FtsTableInfo(tableName, columns, options)
+        }
+    }
+}
diff --git a/annotation/annotation-replacewith-lint/integration-tests/src/main/java/sample/StaticMethodExplicitClass.java b/room/room-runtime/src/jvmMain/kotlin/androidx/room/util/StatementUtil.jvm.kt
similarity index 65%
copy from annotation/annotation-replacewith-lint/integration-tests/src/main/java/sample/StaticMethodExplicitClass.java
copy to room/room-runtime/src/jvmMain/kotlin/androidx/room/util/StatementUtil.jvm.kt
index c5e54fe..62b635e 100644
--- a/annotation/annotation-replacewith-lint/integration-tests/src/main/java/sample/StaticMethodExplicitClass.java
+++ b/room/room-runtime/src/jvmMain/kotlin/androidx/room/util/StatementUtil.jvm.kt
@@ -14,14 +14,14 @@
  * limitations under the License.
  */
 
-package sample;
+@file:JvmMultifileClass
+@file:JvmName("SQLiteStatementUtil")
+
+package androidx.room.util
+
+import androidx.sqlite.SQLiteStatement
 
 /**
- * Usage of a static method with an explicit class.
+ * Returns the zero-based index for the given column name, or -1 if the column doesn't exist.
  */
-@SuppressWarnings({"deprecation", "unused"})
-class StaticMethodExplicitClass {
-    void main() {
-        ReplaceWithUsageJava.toString(this);
-    }
-}
+internal actual fun SQLiteStatement.getColumnIndex(name: String): Int = columnIndexOf(name)
diff --git a/room/room-runtime/src/jvmMain/kotlin/androidx/room/util/TableInfo.jvm.kt b/room/room-runtime/src/jvmMain/kotlin/androidx/room/util/TableInfo.jvm.kt
new file mode 100644
index 0000000..5a8c378
--- /dev/null
+++ b/room/room-runtime/src/jvmMain/kotlin/androidx/room/util/TableInfo.jvm.kt
@@ -0,0 +1,183 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package androidx.room.util
+
+import androidx.annotation.IntDef
+import androidx.annotation.RestrictTo
+import androidx.room.ColumnInfo.SQLiteTypeAffinity
+import androidx.sqlite.SQLiteConnection
+
+/**
+ * A data class that holds the information about a table.
+ *
+ * It directly maps to the result of `PRAGMA table_info(<table_name>)`. Check the
+ * [PRAGMA table_info](http://www.sqlite.org/pragma.html#pragma_table_info)
+ * documentation for more details.
+ *
+ * Even though SQLite column names are case insensitive, this class uses case sensitive matching.
+ */
+@RestrictTo(RestrictTo.Scope.LIBRARY_GROUP_PREFIX)
+actual class TableInfo actual constructor(
+    /**
+     * The table name.
+     */
+    actual val name: String,
+    actual val columns: Map<String, Column>,
+    actual val foreignKeys: Set<ForeignKey>,
+    actual val indices: Set<Index>?
+) {
+    /**
+     * Identifies from where the info object was created.
+     */
+    @Retention(AnnotationRetention.SOURCE)
+    @IntDef(value = [CREATED_FROM_UNKNOWN, CREATED_FROM_ENTITY, CREATED_FROM_DATABASE])
+    internal annotation class CreatedFrom()
+
+    actual override fun equals(other: Any?) = equalsCommon(other)
+
+    actual override fun hashCode() = hashCodeCommon()
+
+    actual override fun toString() = toStringCommon()
+
+    actual companion object {
+        /**
+         * Identifier for when the info is created from an unknown source.
+         */
+        actual const val CREATED_FROM_UNKNOWN = 0
+
+        /**
+         * Identifier for when the info is created from an entity definition, such as generated code
+         * by the compiler or at runtime from a schema bundle, parsed from a schema JSON file.
+         */
+        actual const val CREATED_FROM_ENTITY = 1
+
+        /**
+         * Identifier for when the info is created from the database itself, reading information
+         * from a PRAGMA, such as table_info.
+         */
+        actual const val CREATED_FROM_DATABASE = 2
+
+        /**
+         * Reads the table information from the given database.
+         *
+         * @param connection The database connection to read the information from.
+         * @param tableName The table name.
+         * @return A TableInfo containing the schema information for the provided table name.
+         */
+        actual fun read(connection: SQLiteConnection, tableName: String): TableInfo {
+            return readTableInfo(connection, tableName)
+        }
+    }
+
+    /**
+     * Holds the information about a database column.
+     */
+    @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP_PREFIX)
+    actual class Column actual constructor(
+        /**
+         * The column name.
+         */
+        actual val name: String,
+        /**
+         * The column type affinity.
+         */
+        actual val type: String,
+        /**
+         * Whether or not the column can be NULL.
+         */
+        actual val notNull: Boolean,
+        actual val primaryKeyPosition: Int,
+        actual val defaultValue: String?,
+        @CreatedFrom
+        actual val createdFrom: Int
+    ) {
+        /**
+         * The column type after it is normalized to one of the basic types according to
+         * https://www.sqlite.org/datatype3.html Section 3.1.
+         *
+         * This is the value Room uses for equality check.
+         */
+        @SQLiteTypeAffinity
+        actual val affinity: Int = findAffinity(type)
+
+        /**
+         * Returns whether this column is part of the primary key or not.
+         *
+         * @return True if this column is part of the primary key, false otherwise.
+         */
+        actual val isPrimaryKey: Boolean
+            get() = primaryKeyPosition > 0
+
+        actual override fun equals(other: Any?) = equalsCommon(other)
+
+        actual override fun hashCode() = hashCodeCommon()
+
+        actual override fun toString() = toStringCommon()
+    }
+
+    /**
+     * Holds the information about an SQLite foreign key
+     */
+    @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP_PREFIX)
+    actual class ForeignKey actual constructor(
+        actual val referenceTable: String,
+        actual val onDelete: String,
+        actual val onUpdate: String,
+        actual val columnNames: List<String>,
+        actual val referenceColumnNames: List<String>
+    ) {
+        actual override fun equals(other: Any?) = equalsCommon(other)
+
+        actual override fun hashCode() = hashCodeCommon()
+
+        actual override fun toString() = toStringCommon()
+    }
+
+    /**
+     * Holds the information about an SQLite index
+     */
+    @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP_PREFIX)
+    actual class Index actual constructor(
+        actual val name: String,
+        actual val unique: Boolean,
+        actual val columns: List<String>,
+        actual var orders: List<String>
+    ) {
+        init {
+            orders = orders.ifEmpty {
+                List(columns.size) { androidx.room.Index.Order.ASC.name }
+            }
+        }
+
+        actual companion object {
+            // should match the value in Index.kt
+            actual const val DEFAULT_PREFIX = "index_"
+        }
+
+        actual override fun equals(other: Any?) = equalsCommon(other)
+
+        actual override fun hashCode() = hashCodeCommon()
+
+        actual override fun toString() = toStringCommon()
+    }
+}
+
+/**
+ * Checks if the primary key match.
+ */
+internal actual fun TableInfo.Column.equalsInPrimaryKey(other: TableInfo.Column): Boolean {
+    return isPrimaryKey == other.isPrimaryKey
+}
diff --git a/room/room-runtime/src/jvmMain/kotlin/androidx/room/util/ViewInfo.jvm.kt b/room/room-runtime/src/jvmMain/kotlin/androidx/room/util/ViewInfo.jvm.kt
new file mode 100644
index 0000000..883d803
--- /dev/null
+++ b/room/room-runtime/src/jvmMain/kotlin/androidx/room/util/ViewInfo.jvm.kt
@@ -0,0 +1,57 @@
+/*
+ * Copyright 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package androidx.room.util
+
+import androidx.annotation.RestrictTo
+import androidx.sqlite.SQLiteConnection
+
+/**
+ * A data class that holds the information about a view.
+ *
+ * This derives information from sqlite_master.
+ *
+ * Even though SQLite column names are case insensitive, this class uses case sensitive matching.
+ */
+@RestrictTo(RestrictTo.Scope.LIBRARY_GROUP_PREFIX)
+actual class ViewInfo actual constructor(
+    /**
+     * The view name
+     */
+    actual val name: String,
+    /**
+     * The SQL of CREATE VIEW.
+     */
+    actual val sql: String?
+) {
+    actual override fun equals(other: Any?) = equalsCommon(other)
+
+    actual override fun hashCode() = hashCodeCommon()
+
+    actual override fun toString() = toStringCommon()
+
+    actual companion object {
+        /**
+         * Reads the view information from the given database.
+         *
+         * @param connection The database connection to read the information from.
+         * @param viewName The view name.
+         * @return A ViewInfo containing the schema information for the provided view name.
+         */
+        actual fun read(connection: SQLiteConnection, viewName: String): ViewInfo {
+            return readViewInfo(connection, viewName)
+        }
+    }
+}
diff --git a/room/room-runtime/src/nativeMain/kotlin/androidx/room/InvalidationTracker.native.kt b/room/room-runtime/src/nativeMain/kotlin/androidx/room/InvalidationTracker.native.kt
new file mode 100644
index 0000000..98e4dfe
--- /dev/null
+++ b/room/room-runtime/src/nativeMain/kotlin/androidx/room/InvalidationTracker.native.kt
@@ -0,0 +1,39 @@
+/*
+ * Copyright 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.room
+
+import androidx.annotation.RestrictTo
+import androidx.sqlite.SQLiteConnection
+
+/**
+ * The invalidation tracker keeps track of modified tables by queries and notifies its registered
+ * [Observer]s about such modifications.
+ */
+actual class InvalidationTracker
+@RestrictTo(RestrictTo.Scope.LIBRARY_GROUP_PREFIX)
+actual constructor(
+    database: RoomDatabase,
+    shadowTablesMap: Map<String, String>,
+    viewTables: Map<String, Set<String>>,
+    vararg tableNames: String
+) {
+    /**
+     * Internal method to initialize table tracking. Invoked by generated code.
+     */
+    internal actual fun internalInit(connection: SQLiteConnection) {
+    }
+}
diff --git a/room/room-runtime/src/nativeMain/kotlin/androidx/room/RoomDatabase.native.kt b/room/room-runtime/src/nativeMain/kotlin/androidx/room/RoomDatabase.native.kt
index ae9ee7d..ec3eed9 100644
--- a/room/room-runtime/src/nativeMain/kotlin/androidx/room/RoomDatabase.native.kt
+++ b/room/room-runtime/src/nativeMain/kotlin/androidx/room/RoomDatabase.native.kt
@@ -19,7 +19,9 @@
 import androidx.annotation.RestrictTo
 import androidx.room.migration.AutoMigrationSpec
 import androidx.room.migration.Migration
+import androidx.sqlite.SQLiteConnection
 import androidx.sqlite.SQLiteDriver
+import androidx.sqlite.SQLiteStatement
 import kotlin.reflect.KClass
 
 /**
@@ -38,6 +40,16 @@
     private val typeConverters: MutableMap<KClass<*>, Any> = mutableMapOf()
 
     /**
+     * The invalidation tracker for this database.
+     *
+     * You can use the invalidation tracker to get notified when certain tables in the database
+     * are modified.
+     *
+     * @return The invalidation tracker for the database.
+     */
+    actual val invalidationTracker: InvalidationTracker = createInvalidationTracker()
+
+    /**
      * Called by Room when it is initialized.
      *
      * @param configuration The database configuration.
@@ -78,6 +90,16 @@
     }
 
     /**
+     * Creates the invalidation tracker
+     *
+     * An implementation of this function is generated by the Room processor. Note that this method
+     * is called when the [RoomDatabase] is initialized.
+     *
+     * @return A new invalidation tracker.
+     */
+    protected actual abstract fun createInvalidationTracker(): InvalidationTracker
+
+    /**
      * Returns a Set of required [AutoMigrationSpec] classes.
      *
      * An implementation of this function is generated by the Room processor. Note that this method
@@ -156,6 +178,17 @@
         get() = getRequiredTypeConverterClasses()
 
     /**
+     * Initialize invalidation tracker. Note that this method is called when the [RoomDatabase] is
+     * initialized and opens a database connection.
+     *
+     * @param connection The database connection.
+     */
+    @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
+    protected actual fun internalInitInvalidationTracker(connection: SQLiteConnection) {
+        invalidationTracker.internalInit(connection)
+    }
+
+    /**
      * Closes the database.
      *
      * Once a [RoomDatabase] is closed it should no longer be used.
@@ -165,6 +198,39 @@
     }
 
     /**
+     * Performs a database operation.
+     */
+    @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
+    actual suspend fun <R> perform(
+        isReadOnly: Boolean,
+        sql: String,
+        block: (SQLiteStatement) -> R
+    ): R {
+        return connectionManager.useConnection(isReadOnly) { connection ->
+            connection.usePrepared(sql, block)
+        }
+    }
+
+    /**
+     * Performs a database operation in a transaction.
+     */
+    @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
+    actual suspend fun <R> performTransaction(
+        isReadOnly: Boolean,
+        block: suspend (TransactionScope<R>) -> R
+    ): R {
+        return connectionManager.useConnection(isReadOnly) { transactor ->
+            val type = if (isReadOnly) {
+                Transactor.SQLiteTransactionType.DEFERRED
+            } else {
+                Transactor.SQLiteTransactionType.IMMEDIATE
+            }
+            // TODO: Notify Invalidation Tracker before and after transaction block.
+            transactor.withTransaction(type, block)
+        }
+    }
+
+    /**
      * Journal modes for SQLite database.
      *
      * @see Builder.setJournalMode
diff --git a/room/room-runtime/src/nativeMain/kotlin/androidx/room/util/FtsTableInfo.native.kt b/room/room-runtime/src/nativeMain/kotlin/androidx/room/util/FtsTableInfo.native.kt
new file mode 100644
index 0000000..bc61b67
--- /dev/null
+++ b/room/room-runtime/src/nativeMain/kotlin/androidx/room/util/FtsTableInfo.native.kt
@@ -0,0 +1,66 @@
+/*
+ * Copyright 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package androidx.room.util
+
+import androidx.annotation.RestrictTo
+import androidx.sqlite.SQLiteConnection
+
+/**
+ * A data class that holds the information about an FTS table.
+ *
+ */
+@RestrictTo(RestrictTo.Scope.LIBRARY_GROUP_PREFIX)
+actual class FtsTableInfo(
+    /**
+     * The table name
+     */
+    actual val name: String,
+
+    /**
+     * The column names
+     */
+    actual val columns: Set<String>,
+
+    /**
+     * The set of options. Each value in the set contains the option in the following format:
+     * <key, value>.
+     */
+    actual val options: Set<String>
+) {
+    actual constructor(name: String, columns: Set<String>, createSql: String) :
+        this(name, columns, parseFtsOptions(createSql))
+
+    override fun equals(other: Any?) = equalsCommon(other)
+
+    override fun hashCode() = hashCodeCommon()
+
+    override fun toString() = toStringCommon()
+
+    actual companion object {
+        /**
+         * Reads the table information from the given database.
+         *
+         * @param connection The database connection to read the information from.
+         * @param tableName The table name.
+         * @return A FtsTableInfo containing the columns and options for the provided table name.
+         */
+        actual fun read(connection: SQLiteConnection, tableName: String): FtsTableInfo {
+            val columns = readFtsColumns(connection, tableName)
+            val options = readFtsOptions(connection, tableName)
+            return FtsTableInfo(tableName, columns, options)
+        }
+    }
+}
diff --git a/room/room-runtime/src/nativeMain/kotlin/androidx/room/util/KClassUtil.kt b/room/room-runtime/src/nativeMain/kotlin/androidx/room/util/KClassUtil.native.kt
similarity index 100%
rename from room/room-runtime/src/nativeMain/kotlin/androidx/room/util/KClassUtil.kt
rename to room/room-runtime/src/nativeMain/kotlin/androidx/room/util/KClassUtil.native.kt
diff --git a/annotation/annotation-replacewith-lint/integration-tests/src/main/java/sample/StaticMethodExplicitClass.java b/room/room-runtime/src/nativeMain/kotlin/androidx/room/util/StatementUtil.native.kt
similarity index 61%
copy from annotation/annotation-replacewith-lint/integration-tests/src/main/java/sample/StaticMethodExplicitClass.java
copy to room/room-runtime/src/nativeMain/kotlin/androidx/room/util/StatementUtil.native.kt
index c5e54fe..437fe5c 100644
--- a/annotation/annotation-replacewith-lint/integration-tests/src/main/java/sample/StaticMethodExplicitClass.java
+++ b/room/room-runtime/src/nativeMain/kotlin/androidx/room/util/StatementUtil.native.kt
@@ -14,14 +14,16 @@
  * limitations under the License.
  */
 
-package sample;
+@file:JvmMultifileClass
+@file:JvmName("SQLiteStatementUtil")
+
+package androidx.room.util
+
+import androidx.sqlite.SQLiteStatement
+import kotlin.jvm.JvmMultifileClass
+import kotlin.jvm.JvmName
 
 /**
- * Usage of a static method with an explicit class.
+ * Returns the zero-based index for the given column name, or -1 if the column doesn't exist.
  */
-@SuppressWarnings({"deprecation", "unused"})
-class StaticMethodExplicitClass {
-    void main() {
-        ReplaceWithUsageJava.toString(this);
-    }
-}
+internal actual fun SQLiteStatement.getColumnIndex(name: String): Int = columnIndexOf(name)
diff --git a/room/room-runtime/src/nativeMain/kotlin/androidx/room/util/TableInfo.native.kt b/room/room-runtime/src/nativeMain/kotlin/androidx/room/util/TableInfo.native.kt
new file mode 100644
index 0000000..5a8c378
--- /dev/null
+++ b/room/room-runtime/src/nativeMain/kotlin/androidx/room/util/TableInfo.native.kt
@@ -0,0 +1,183 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package androidx.room.util
+
+import androidx.annotation.IntDef
+import androidx.annotation.RestrictTo
+import androidx.room.ColumnInfo.SQLiteTypeAffinity
+import androidx.sqlite.SQLiteConnection
+
+/**
+ * A data class that holds the information about a table.
+ *
+ * It directly maps to the result of `PRAGMA table_info(<table_name>)`. Check the
+ * [PRAGMA table_info](http://www.sqlite.org/pragma.html#pragma_table_info)
+ * documentation for more details.
+ *
+ * Even though SQLite column names are case insensitive, this class uses case sensitive matching.
+ */
+@RestrictTo(RestrictTo.Scope.LIBRARY_GROUP_PREFIX)
+actual class TableInfo actual constructor(
+    /**
+     * The table name.
+     */
+    actual val name: String,
+    actual val columns: Map<String, Column>,
+    actual val foreignKeys: Set<ForeignKey>,
+    actual val indices: Set<Index>?
+) {
+    /**
+     * Identifies from where the info object was created.
+     */
+    @Retention(AnnotationRetention.SOURCE)
+    @IntDef(value = [CREATED_FROM_UNKNOWN, CREATED_FROM_ENTITY, CREATED_FROM_DATABASE])
+    internal annotation class CreatedFrom()
+
+    actual override fun equals(other: Any?) = equalsCommon(other)
+
+    actual override fun hashCode() = hashCodeCommon()
+
+    actual override fun toString() = toStringCommon()
+
+    actual companion object {
+        /**
+         * Identifier for when the info is created from an unknown source.
+         */
+        actual const val CREATED_FROM_UNKNOWN = 0
+
+        /**
+         * Identifier for when the info is created from an entity definition, such as generated code
+         * by the compiler or at runtime from a schema bundle, parsed from a schema JSON file.
+         */
+        actual const val CREATED_FROM_ENTITY = 1
+
+        /**
+         * Identifier for when the info is created from the database itself, reading information
+         * from a PRAGMA, such as table_info.
+         */
+        actual const val CREATED_FROM_DATABASE = 2
+
+        /**
+         * Reads the table information from the given database.
+         *
+         * @param connection The database connection to read the information from.
+         * @param tableName The table name.
+         * @return A TableInfo containing the schema information for the provided table name.
+         */
+        actual fun read(connection: SQLiteConnection, tableName: String): TableInfo {
+            return readTableInfo(connection, tableName)
+        }
+    }
+
+    /**
+     * Holds the information about a database column.
+     */
+    @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP_PREFIX)
+    actual class Column actual constructor(
+        /**
+         * The column name.
+         */
+        actual val name: String,
+        /**
+         * The column type affinity.
+         */
+        actual val type: String,
+        /**
+         * Whether or not the column can be NULL.
+         */
+        actual val notNull: Boolean,
+        actual val primaryKeyPosition: Int,
+        actual val defaultValue: String?,
+        @CreatedFrom
+        actual val createdFrom: Int
+    ) {
+        /**
+         * The column type after it is normalized to one of the basic types according to
+         * https://www.sqlite.org/datatype3.html Section 3.1.
+         *
+         * This is the value Room uses for equality check.
+         */
+        @SQLiteTypeAffinity
+        actual val affinity: Int = findAffinity(type)
+
+        /**
+         * Returns whether this column is part of the primary key or not.
+         *
+         * @return True if this column is part of the primary key, false otherwise.
+         */
+        actual val isPrimaryKey: Boolean
+            get() = primaryKeyPosition > 0
+
+        actual override fun equals(other: Any?) = equalsCommon(other)
+
+        actual override fun hashCode() = hashCodeCommon()
+
+        actual override fun toString() = toStringCommon()
+    }
+
+    /**
+     * Holds the information about an SQLite foreign key
+     */
+    @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP_PREFIX)
+    actual class ForeignKey actual constructor(
+        actual val referenceTable: String,
+        actual val onDelete: String,
+        actual val onUpdate: String,
+        actual val columnNames: List<String>,
+        actual val referenceColumnNames: List<String>
+    ) {
+        actual override fun equals(other: Any?) = equalsCommon(other)
+
+        actual override fun hashCode() = hashCodeCommon()
+
+        actual override fun toString() = toStringCommon()
+    }
+
+    /**
+     * Holds the information about an SQLite index
+     */
+    @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP_PREFIX)
+    actual class Index actual constructor(
+        actual val name: String,
+        actual val unique: Boolean,
+        actual val columns: List<String>,
+        actual var orders: List<String>
+    ) {
+        init {
+            orders = orders.ifEmpty {
+                List(columns.size) { androidx.room.Index.Order.ASC.name }
+            }
+        }
+
+        actual companion object {
+            // should match the value in Index.kt
+            actual const val DEFAULT_PREFIX = "index_"
+        }
+
+        actual override fun equals(other: Any?) = equalsCommon(other)
+
+        actual override fun hashCode() = hashCodeCommon()
+
+        actual override fun toString() = toStringCommon()
+    }
+}
+
+/**
+ * Checks if the primary key match.
+ */
+internal actual fun TableInfo.Column.equalsInPrimaryKey(other: TableInfo.Column): Boolean {
+    return isPrimaryKey == other.isPrimaryKey
+}
diff --git a/room/room-runtime/src/nativeMain/kotlin/androidx/room/util/ViewInfo.native.kt b/room/room-runtime/src/nativeMain/kotlin/androidx/room/util/ViewInfo.native.kt
new file mode 100644
index 0000000..883d803
--- /dev/null
+++ b/room/room-runtime/src/nativeMain/kotlin/androidx/room/util/ViewInfo.native.kt
@@ -0,0 +1,57 @@
+/*
+ * Copyright 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package androidx.room.util
+
+import androidx.annotation.RestrictTo
+import androidx.sqlite.SQLiteConnection
+
+/**
+ * A data class that holds the information about a view.
+ *
+ * This derives information from sqlite_master.
+ *
+ * Even though SQLite column names are case insensitive, this class uses case sensitive matching.
+ */
+@RestrictTo(RestrictTo.Scope.LIBRARY_GROUP_PREFIX)
+actual class ViewInfo actual constructor(
+    /**
+     * The view name
+     */
+    actual val name: String,
+    /**
+     * The SQL of CREATE VIEW.
+     */
+    actual val sql: String?
+) {
+    actual override fun equals(other: Any?) = equalsCommon(other)
+
+    actual override fun hashCode() = hashCodeCommon()
+
+    actual override fun toString() = toStringCommon()
+
+    actual companion object {
+        /**
+         * Reads the view information from the given database.
+         *
+         * @param connection The database connection to read the information from.
+         * @param viewName The view name.
+         * @return A ViewInfo containing the schema information for the provided view name.
+         */
+        actual fun read(connection: SQLiteConnection, viewName: String): ViewInfo {
+            return readViewInfo(connection, viewName)
+        }
+    }
+}
diff --git a/room/room-runtime/src/nativeTest/kotlin/androidx.room/BuilderTest_TestDatabase_Impl.kt b/room/room-runtime/src/nativeTest/kotlin/androidx.room/BuilderTest_TestDatabase_Impl.kt
index 9ad6b2d..9d3f9c2 100644
--- a/room/room-runtime/src/nativeTest/kotlin/androidx.room/BuilderTest_TestDatabase_Impl.kt
+++ b/room/room-runtime/src/nativeTest/kotlin/androidx.room/BuilderTest_TestDatabase_Impl.kt
@@ -36,6 +36,10 @@
         }
     }
 
+    override fun createInvalidationTracker(): InvalidationTracker {
+        return InvalidationTracker(this, emptyMap(), emptyMap())
+    }
+
     override fun createAutoMigrations(
         autoMigrationSpecs: Map<KClass<out AutoMigrationSpec>, AutoMigrationSpec>
     ): List<Migration> {
diff --git a/settings.gradle b/settings.gradle
index c5cec53..c0ab326 100644
--- a/settings.gradle
+++ b/settings.gradle
@@ -340,9 +340,6 @@
 includeProject(":annotation:annotation-experimental")
 includeProject(":annotation:annotation-experimental-lint")
 includeProject(":annotation:annotation-experimental-lint-integration-tests", "annotation/annotation-experimental-lint/integration-tests")
-includeProject(":annotation:annotation-replacewith")
-includeProject(":annotation:annotation-replacewith-lint")
-includeProject(":annotation:annotation-replacewith-lint-integration-tests", "annotation/annotation-replacewith-lint/integration-tests")
 includeProject(":annotation:annotation-sampled")
 includeProject(":appactions:builtintypes:builtintypes", [BuildType.MAIN])
 includeProject(":appactions:builtintypes:builtintypes:builtintypes-samples", "appactions/builtintypes/builtintypes/samples", [BuildType.MAIN])
@@ -742,7 +739,7 @@
 includeProject(":lifecycle:integration-tests:incrementality", [BuildType.MAIN, BuildType.FLAN])
 includeProject(":lifecycle:integration-tests:lifecycle-testapp", "lifecycle/integration-tests/testapp", [BuildType.MAIN, BuildType.FLAN])
 includeProject(":lifecycle:integration-tests:lifecycle-testapp-kotlin", "lifecycle/integration-tests/kotlintestapp", [BuildType.MAIN, BuildType.FLAN])
-includeProject(":lifecycle:lifecycle-common", [BuildType.MAIN, BuildType.FLAN, BuildType.COMPOSE])
+includeProject(":lifecycle:lifecycle-common", [BuildType.MAIN, BuildType.FLAN, BuildType.COMPOSE, BuildType.INFRAROGUE, BuildType.KMP])
 includeProject(":lifecycle:lifecycle-common-java8", [BuildType.MAIN, BuildType.FLAN, BuildType.COMPOSE])
 includeProject(":lifecycle:lifecycle-compiler", [BuildType.MAIN, BuildType.FLAN])
 includeProject(":lifecycle:lifecycle-extensions", [BuildType.MAIN, BuildType.FLAN])
@@ -770,6 +767,7 @@
 includeProject(":lifecycle:lifecycle-viewmodel-compose:integration-tests:lifecycle-viewmodel-demos", [BuildType.COMPOSE])
 includeProject(":lifecycle:lifecycle-viewmodel-ktx", [BuildType.MAIN, BuildType.FLAN, BuildType.COMPOSE])
 includeProject(":lifecycle:lifecycle-viewmodel-savedstate", [BuildType.MAIN, BuildType.FLAN, BuildType.COMPOSE])
+includeProject(":lint:lint-gradle", [BuildType.MAIN])
 includeProject(":lint-checks")
 includeProject(":lint-checks:integration-tests")
 includeProject(":loader:loader", [BuildType.MAIN])
diff --git a/sqlite/integration-tests/driver-conformance-test/src/commonTest/kotlin/androidx/sqlite/driver/test/BaseConformanceTest.kt b/sqlite/integration-tests/driver-conformance-test/src/commonTest/kotlin/androidx/sqlite/driver/test/BaseConformanceTest.kt
index ef5bed9..4b9926c 100644
--- a/sqlite/integration-tests/driver-conformance-test/src/commonTest/kotlin/androidx/sqlite/driver/test/BaseConformanceTest.kt
+++ b/sqlite/integration-tests/driver-conformance-test/src/commonTest/kotlin/androidx/sqlite/driver/test/BaseConformanceTest.kt
@@ -188,15 +188,8 @@
     fun readColumnNameWithoutStep() = testWithConnection { connection ->
         connection.execSQL("CREATE TABLE Test (col)")
         connection.prepare("SELECT col FROM Test").use {
-            if (driverType == TestDriverType.ANDROID_FRAMEWORK) {
-                // In Android framework the statement is not compiled until first step, so
-                // proper analysis of result column count cannot be done.
-                val message = assertFailsWith<SQLiteException> { it.getColumnName(0) }.message
-                assertThat(message).isEqualTo("Error code: 21, message: no row")
-            } else {
-                assertThat(it.getColumnCount()).isEqualTo(1)
-                assertThat(it.getColumnName(0)).isEqualTo("col")
-            }
+            assertThat(it.getColumnCount()).isEqualTo(1)
+            assertThat(it.getColumnName(0)).isEqualTo("col")
         }
     }
 
diff --git a/sqlite/sqlite-framework/src/androidMain/kotlin/androidx/sqlite/driver/AndroidSQLiteStatement.android.kt b/sqlite/sqlite-framework/src/androidMain/kotlin/androidx/sqlite/driver/AndroidSQLiteStatement.android.kt
index 61a2221..1ee0693 100644
--- a/sqlite/sqlite-framework/src/androidMain/kotlin/androidx/sqlite/driver/AndroidSQLiteStatement.android.kt
+++ b/sqlite/sqlite-framework/src/androidMain/kotlin/androidx/sqlite/driver/AndroidSQLiteStatement.android.kt
@@ -141,30 +141,22 @@
 
         override fun getColumnCount(): Int {
             throwIfClosed()
+            ensureCursor()
             return cursor?.columnCount ?: 0
         }
 
         override fun getColumnName(index: Int): String {
             throwIfClosed()
-            val c = throwIfNoRow()
+            ensureCursor()
+            val c = checkNotNull(cursor)
             throwIfInvalidColumn(c, index)
             return c.getColumnName(index)
         }
 
         override fun step(): Boolean {
             throwIfClosed()
-            if (cursor == null) {
-                cursor = db.rawQueryWithFactory(
-                    /* cursorFactory = */ { _, masterQuery, editTable, query ->
-                        bindTo(query)
-                        SQLiteCursor(masterQuery, editTable, query)
-                    },
-                    /* sql = */ sql,
-                    /* selectionArgs = */ arrayOfNulls(0),
-                    /* editTable = */ null
-                )
-            }
-            return requireNotNull(cursor).moveToNext()
+            ensureCursor()
+            return checkNotNull(cursor).moveToNext()
         }
 
         override fun reset() {
@@ -218,6 +210,20 @@
             }
         }
 
+        private fun ensureCursor() {
+            if (cursor == null) {
+                cursor = db.rawQueryWithFactory(
+                    /* cursorFactory = */ { _, masterQuery, editTable, query ->
+                        bindTo(query)
+                        SQLiteCursor(masterQuery, editTable, query)
+                    },
+                    /* sql = */ sql,
+                    /* selectionArgs = */ arrayOfNulls(0),
+                    /* editTable = */ null
+                )
+            }
+        }
+
         private fun bindTo(query: SQLiteProgram) {
             for (index in 1 until bindingTypes.size) {
                 when (bindingTypes[index]) {
diff --git a/stableaidl/stableaidl-gradle-plugin/src/main/java/androidx/stableaidl/StableAidlTasks.kt b/stableaidl/stableaidl-gradle-plugin/src/main/java/androidx/stableaidl/StableAidlTasks.kt
index 93bcbf8..23ab4ad 100644
--- a/stableaidl/stableaidl-gradle-plugin/src/main/java/androidx/stableaidl/StableAidlTasks.kt
+++ b/stableaidl/stableaidl-gradle-plugin/src/main/java/androidx/stableaidl/StableAidlTasks.kt
@@ -133,7 +133,7 @@
 private fun DomainObjectCollection<ConfigurationVariant>.allNamed(
     name: String,
     action: Action<ConfigurationVariant>
-) = all { variant ->
+) = configureEach { variant ->
     if (variant.name == name) {
         action.execute(variant)
     }
diff --git a/test/ext/junit-gtest/src/main/cpp/CMakeLists.txt b/test/ext/junit-gtest/src/main/cpp/CMakeLists.txt
index 1c8f7e4..c563898 100644
--- a/test/ext/junit-gtest/src/main/cpp/CMakeLists.txt
+++ b/test/ext/junit-gtest/src/main/cpp/CMakeLists.txt
@@ -26,4 +26,4 @@
         -uJava_androidx_test_ext_junitgtest_GtestRunner_initialize
         -uJava_androidx_test_ext_junitgtest_GtestRunner_run
         -uJava_androidx_test_ext_junitgtest_GtestRunner_addTest
-        )
\ No newline at end of file
+        )
diff --git a/test/integration-tests/junit-gtest-test/src/main/cpp/CMakeLists.txt b/test/integration-tests/junit-gtest-test/src/main/cpp/CMakeLists.txt
index f88adb0..cd8a95e 100644
--- a/test/integration-tests/junit-gtest-test/src/main/cpp/CMakeLists.txt
+++ b/test/integration-tests/junit-gtest-test/src/main/cpp/CMakeLists.txt
@@ -21,4 +21,12 @@
         adder
         googletest::gtest
         junit-gtest::junit-gtest
-        )
\ No newline at end of file
+        )
+target_link_options(adder
+        PRIVATE
+        "-Wl,-z,max-page-size=16384"
+)
+target_link_options(apptest
+        PRIVATE
+        "-Wl,-z,max-page-size=16384"
+)
diff --git a/test/uiautomator/uiautomator/src/main/java/androidx/test/uiautomator/GestureController.java b/test/uiautomator/uiautomator/src/main/java/androidx/test/uiautomator/GestureController.java
index 742176d..6366d96 100644
--- a/test/uiautomator/uiautomator/src/main/java/androidx/test/uiautomator/GestureController.java
+++ b/test/uiautomator/uiautomator/src/main/java/androidx/test/uiautomator/GestureController.java
@@ -25,7 +25,9 @@
 import android.view.MotionEvent.PointerCoords;
 import android.view.MotionEvent.PointerProperties;
 
+import androidx.annotation.DoNotInline;
 import androidx.annotation.NonNull;
+import androidx.annotation.RequiresApi;
 
 import java.lang.reflect.Method;
 import java.util.ArrayList;
@@ -129,12 +131,20 @@
         // Record the start time
         final long startTime = SystemClock.uptimeMillis();
 
-        // Update motion event delay to twice of the display refresh rate
+        // Update motion event delay to twice of the maximum display refresh rate
         long injectionDelay = MOTION_EVENT_INJECTION_DELAY_MILLIS;
         try {
             int displayId = pending.peek().displayId();
             Display display = mDevice.getDisplayById(displayId);
-            float displayRefreshRate = display.getRefreshRate();
+            float displayRefreshRate;
+            if (android.os.Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.LOLLIPOP) {
+                float[] refreshRates = Api21Impl.getSupportedRefreshRates(display);
+                Arrays.sort(refreshRates);
+                displayRefreshRate = refreshRates[refreshRates.length - 1];
+            } else {
+                // Set to current refresh rate if API version is lower than 21.
+                displayRefreshRate = display.getRefreshRate();
+            }
             injectionDelay = (long) (500 / displayRefreshRate);
         } catch (Exception e) {
             Log.e(TAG, "Fail to update motion event delay", e);
@@ -295,4 +305,15 @@
     UiDevice getDevice() {
         return mDevice;
     }
+
+    @RequiresApi(21)
+    private static class Api21Impl {
+        private Api21Impl() {
+        }
+
+        @DoNotInline
+        static float[] getSupportedRefreshRates(Display display) {
+            return display.getSupportedRefreshRates();
+        }
+    }
 }
diff --git a/tracing/tracing-perfetto-binary/src/main/cpp/CMakeLists.txt b/tracing/tracing-perfetto-binary/src/main/cpp/CMakeLists.txt
index 59111c1..4c98782 100644
--- a/tracing/tracing-perfetto-binary/src/main/cpp/CMakeLists.txt
+++ b/tracing/tracing-perfetto-binary/src/main/cpp/CMakeLists.txt
@@ -29,3 +29,4 @@
 find_library(log-lib log)
 
 target_link_libraries(tracing_perfetto ${android-lib} ${log-lib} perfetto ${CMAKE_THREAD_LIBS_INIT})
+target_link_options(tracing_perfetto PRIVATE "-Wl,-z,max-page-size=16384")
diff --git a/wear/compose/compose-foundation/build.gradle b/wear/compose/compose-foundation/build.gradle
index 09e776f..d54bbb7 100644
--- a/wear/compose/compose-foundation/build.gradle
+++ b/wear/compose/compose-foundation/build.gradle
@@ -40,7 +40,7 @@
     implementation(libs.kotlinStdlib)
     implementation(project(":compose:foundation:foundation-layout"))
     implementation(project(":compose:ui:ui-util"))
-    implementation(project(":lifecycle:lifecycle-runtime-compose"))
+    implementation("androidx.lifecycle:lifecycle-runtime-compose:2.7.0")
     implementation("androidx.core:core:1.12.0")
     implementation("androidx.profileinstaller:profileinstaller:1.3.0")
 
diff --git a/wear/compose/compose-material-core/src/androidTest/kotlin/androidx/wear/compose/materialcore/ButtonTest.kt b/wear/compose/compose-material-core/src/androidTest/kotlin/androidx/wear/compose/materialcore/RoundButtonTest.kt
similarity index 94%
rename from wear/compose/compose-material-core/src/androidTest/kotlin/androidx/wear/compose/materialcore/ButtonTest.kt
rename to wear/compose/compose-material-core/src/androidTest/kotlin/androidx/wear/compose/materialcore/RoundButtonTest.kt
index d900da3..31a9ecd 100644
--- a/wear/compose/compose-material-core/src/androidTest/kotlin/androidx/wear/compose/materialcore/ButtonTest.kt
+++ b/wear/compose/compose-material-core/src/androidTest/kotlin/androidx/wear/compose/materialcore/RoundButtonTest.kt
@@ -60,14 +60,14 @@
 import org.junit.Rule
 import org.junit.Test
 
-class ButtonTest {
+class RoundButtonTest {
     @get:Rule
     val rule = createComposeRule()
 
     @Test
     fun supports_testtag_on_button() {
         rule.setContent {
-            ButtonWithDefaults(
+            RoundButtonWithDefaults(
                 modifier = Modifier.testTag(TEST_TAG),
             ) {
             }
@@ -79,7 +79,7 @@
     @Test
     fun has_clickaction_when_enabled() {
         rule.setContent {
-            ButtonWithDefaults(
+            RoundButtonWithDefaults(
                 enabled = true,
                 modifier = Modifier.testTag(TEST_TAG)
             ) {
@@ -92,7 +92,7 @@
     @Test
     fun has_clickaction_when_disabled() {
         rule.setContent {
-            ButtonWithDefaults(
+            RoundButtonWithDefaults(
                 enabled = false,
                 modifier = Modifier.testTag(TEST_TAG)
             ) {
@@ -105,7 +105,7 @@
     @Test
     fun is_correctly_enabled() {
         rule.setContent {
-            ButtonWithDefaults(
+            RoundButtonWithDefaults(
                 enabled = true,
                 modifier = Modifier.testTag(TEST_TAG)
             ) {
@@ -118,7 +118,7 @@
     @Test
     fun is_correctly_disabled() {
         rule.setContent {
-            ButtonWithDefaults(
+            RoundButtonWithDefaults(
                 enabled = false,
                 modifier = Modifier.testTag(TEST_TAG)
             ) {
@@ -133,7 +133,7 @@
         var clicked = false
 
         rule.setContent {
-            ButtonWithDefaults(
+            RoundButtonWithDefaults(
                 onClick = { clicked = true },
                 enabled = true,
                 modifier = Modifier.testTag(TEST_TAG)
@@ -153,7 +153,7 @@
         var clicked = false
 
         rule.setContent {
-            ButtonWithDefaults(
+            RoundButtonWithDefaults(
                 onClick = { clicked = true },
                 enabled = false,
                 modifier = Modifier.testTag(TEST_TAG)
@@ -171,7 +171,7 @@
     @Test
     fun has_role_button_for_button() {
         rule.setContent {
-            ButtonWithDefaults(
+            RoundButtonWithDefaults(
                 modifier = Modifier.testTag(TEST_TAG)
             ) {
             }
@@ -189,7 +189,7 @@
     @Test
     fun supports_circleshape_under_ltr_for_button() =
         rule.isShape(CircleShape, LayoutDirection.Ltr) {
-            ButtonWithDefaults(
+            RoundButtonWithDefaults(
                 modifier = Modifier.testTag(TEST_TAG),
             ) {
             }
@@ -198,7 +198,7 @@
     @Test
     fun supports_circleshape_under_rtl_for_button() =
         rule.isShape(CircleShape, LayoutDirection.Rtl) {
-            ButtonWithDefaults(
+            RoundButtonWithDefaults(
                 modifier = Modifier.testTag(TEST_TAG),
             ) {
             }
@@ -207,7 +207,7 @@
     @Test
     fun extra_small_button_meets_accessibility_tapsize() {
         verifyTapSize(48.dp) {
-            ButtonWithDefaults(
+            RoundButtonWithDefaults(
                 modifier = Modifier
                     .testTag(TEST_TAG)
                     .size(32.dp)
@@ -219,7 +219,7 @@
     @Test
     fun extra_small_button_has_correct_visible_size() {
         verifyVisibleSize(32.dp) {
-            ButtonWithDefaults(
+            RoundButtonWithDefaults(
                 modifier = Modifier
                     .testTag(TEST_TAG)
                     .requiredSize(32.dp)
@@ -232,7 +232,7 @@
     fun default_button_has_correct_tapsize() {
         // Tap size for Button should be the min button size.
         verifyTapSize(52.dp) {
-            ButtonWithDefaults(
+            RoundButtonWithDefaults(
                 modifier = Modifier
                     .testTag(TEST_TAG)
             ) {
@@ -244,7 +244,7 @@
     fun default_button_has_correct_visible_size() {
         // Tap size for Button should be the min button size.
         verifyVisibleSize(52.dp) {
-            ButtonWithDefaults(
+            RoundButtonWithDefaults(
                 modifier = Modifier
                     .testTag(TEST_TAG)
                     .size(52.dp)
@@ -258,7 +258,7 @@
         val shape = CutCornerShape(4.dp)
 
         rule.isShape(shape, LayoutDirection.Ltr) {
-            ButtonWithDefaults(
+            RoundButtonWithDefaults(
                 shape = shape,
                 modifier = Modifier.testTag(TEST_TAG)
             ) {
@@ -298,7 +298,7 @@
 
         rule.setContent {
             Box(modifier = Modifier.fillMaxSize()) {
-                ButtonWithDefaults(
+                RoundButtonWithDefaults(
                     content = {
                         CompositionLocalProvider(
                             LocalContentTestData provides EXPECTED_LOCAL_TEST_DATA
@@ -339,7 +339,7 @@
                     .fillMaxSize()
                     .background(testBackground)
             ) {
-                ButtonWithDefaults(
+                RoundButtonWithDefaults(
                     backgroundColor = { enabled ->
                         if (enabled) enabledBackgroundColor else disabledBackgroundColor
                     },
@@ -388,7 +388,7 @@
     }
 
     @Composable
-    internal fun ButtonWithDefaults(
+    internal fun RoundButtonWithDefaults(
         modifier: Modifier = Modifier,
         onClick: () -> Unit = {},
         enabled: Boolean = true,
@@ -397,7 +397,7 @@
         shape: Shape = CircleShape,
         border: @Composable (enabled: Boolean) -> BorderStroke? = { null },
         content: @Composable BoxScope.() -> Unit
-    ) = Button(
+    ) = RoundButton(
         onClick = onClick,
         modifier = modifier,
         enabled = enabled,
diff --git a/wear/compose/compose-material-core/src/main/java/androidx/wear/compose/materialcore/Button.kt b/wear/compose/compose-material-core/src/main/java/androidx/wear/compose/materialcore/RoundButton.kt
similarity index 91%
rename from wear/compose/compose-material-core/src/main/java/androidx/wear/compose/materialcore/Button.kt
rename to wear/compose/compose-material-core/src/main/java/androidx/wear/compose/materialcore/RoundButton.kt
index c651af3..86a7f66 100644
--- a/wear/compose/compose-material-core/src/main/java/androidx/wear/compose/materialcore/Button.kt
+++ b/wear/compose/compose-material-core/src/main/java/androidx/wear/compose/materialcore/RoundButton.kt
@@ -38,9 +38,10 @@
 import androidx.compose.ui.unit.Dp
 
 /**
- * Wear Material [Button] that offers a single slot to take any content (text, icon or image).
+ * Wear Material [RoundButton] that offers a single slot to take any content (text, icon or image)
+ * and is round/circular in shape.
  *
- * [Button] can be enabled or disabled. A disabled button will not respond to click events.
+ * [RoundButton] can be enabled or disabled. A disabled button will not respond to click events.
  *
  * For more information, see the
  * [Buttons](https://developer.android.com/training/wearables/components/buttons)
@@ -59,11 +60,11 @@
  * @param border Resolves the border for this button in different states.
  * @param buttonSize The default size of the button unless overridden by Modifier.size.
  * @param ripple Ripple used for this button.
- * @param content The content displayed on the [Button] such as text, icon or image.
+ * @param content The content displayed on the [RoundButton] such as text, icon or image.
  */
 @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
 @Composable
-fun Button(
+fun RoundButton(
     onClick: () -> Unit,
     modifier: Modifier,
     enabled: Boolean,
diff --git a/wear/compose/compose-material/build.gradle b/wear/compose/compose-material/build.gradle
index b7f87f3..9073f97 100644
--- a/wear/compose/compose-material/build.gradle
+++ b/wear/compose/compose-material/build.gradle
@@ -44,7 +44,7 @@
     implementation(project(":compose:ui:ui-util"))
     implementation(project(":wear:compose:compose-material-core"))
     implementation("androidx.profileinstaller:profileinstaller:1.3.0")
-    implementation("androidx.lifecycle:lifecycle-common:2.5.1")
+    implementation("androidx.lifecycle:lifecycle-common:2.7.0")
 
     androidTestImplementation(project(":compose:ui:ui-test"))
     androidTestImplementation(project(":compose:ui:ui-test-junit4"))
diff --git a/wear/compose/compose-material/src/main/java/androidx/wear/compose/material/Button.kt b/wear/compose/compose-material/src/main/java/androidx/wear/compose/material/Button.kt
index da69959..67e5b3c0 100644
--- a/wear/compose/compose-material/src/main/java/androidx/wear/compose/material/Button.kt
+++ b/wear/compose/compose-material/src/main/java/androidx/wear/compose/material/Button.kt
@@ -154,7 +154,7 @@
     border: ButtonBorder = ButtonDefaults.buttonBorder(),
     content: @Composable BoxScope.() -> Unit,
 ) {
-    androidx.wear.compose.materialcore.Button(
+    androidx.wear.compose.materialcore.RoundButton(
         onClick = onClick,
         modifier = modifier,
         enabled = enabled,
@@ -335,7 +335,7 @@
     border: ButtonBorder = ButtonDefaults.buttonBorder(),
     content: @Composable BoxScope.() -> Unit,
 ) {
-    androidx.wear.compose.materialcore.Button(
+    androidx.wear.compose.materialcore.RoundButton(
         onClick = onClick,
         modifier = modifier
             .padding(backgroundPadding)
diff --git a/wear/compose/compose-material/src/main/java/androidx/wear/compose/material/ListHeader.kt b/wear/compose/compose-material/src/main/java/androidx/wear/compose/material/ListHeader.kt
index 994e12c..a7c7a0c 100644
--- a/wear/compose/compose-material/src/main/java/androidx/wear/compose/material/ListHeader.kt
+++ b/wear/compose/compose-material/src/main/java/androidx/wear/compose/material/ListHeader.kt
@@ -17,8 +17,10 @@
 package androidx.wear.compose.material
 
 import androidx.compose.foundation.background
+import androidx.compose.foundation.layout.IntrinsicSize
 import androidx.compose.foundation.layout.Row
 import androidx.compose.foundation.layout.RowScope
+import androidx.compose.foundation.layout.defaultMinSize
 import androidx.compose.foundation.layout.height
 import androidx.compose.foundation.layout.padding
 import androidx.compose.foundation.layout.wrapContentSize
@@ -33,7 +35,7 @@
 /**
  * A slot based composable for creating a list header item. List header items are typically expected
  * to be text. The contents provided will have text and colors effects applied based on the
- * MaterialTheme. The contents will be start and end padded.
+ * MaterialTheme. The contents will be start and end padded and should cover up to 3 lines of text.
  *
  * Example usage:
  * @sample androidx.wear.compose.material.samples.ScalingLazyColumnWithHeaders
@@ -50,7 +52,9 @@
     content: @Composable RowScope.() -> Unit
 ) {
     Row(
-        modifier = modifier.height(48.dp)
+        modifier = modifier
+            .defaultMinSize(minHeight = 48.dp)
+            .height(IntrinsicSize.Min)
             .wrapContentSize()
             .background(backgroundColor)
             .padding(horizontal = 14.dp)
diff --git a/wear/compose/compose-material3/api/current.txt b/wear/compose/compose-material3/api/current.txt
index f4f6ce2..d480645 100644
--- a/wear/compose/compose-material3/api/current.txt
+++ b/wear/compose/compose-material3/api/current.txt
@@ -27,8 +27,6 @@
     method @androidx.compose.runtime.Composable public androidx.wear.compose.material3.ButtonColors buttonColors(optional long containerColor, optional long contentColor, optional long secondaryContentColor, optional long iconColor, optional long disabledContainerColor, optional long disabledContentColor, optional long disabledSecondaryContentColor, optional long disabledIconColor);
     method @androidx.compose.runtime.Composable public androidx.wear.compose.material3.ButtonColors childButtonColors();
     method @androidx.compose.runtime.Composable public androidx.wear.compose.material3.ButtonColors childButtonColors(optional long contentColor, optional long secondaryContentColor, optional long iconColor, optional long disabledContentColor, optional long disabledSecondaryContentColor, optional long disabledIconColor);
-    method @androidx.compose.runtime.Composable public androidx.wear.compose.material3.ButtonColors filledButtonColors();
-    method @androidx.compose.runtime.Composable public androidx.wear.compose.material3.ButtonColors filledButtonColors(optional long containerColor, optional long contentColor, optional long secondaryContentColor, optional long iconColor, optional long disabledContainerColor, optional long disabledContentColor, optional long disabledSecondaryContentColor, optional long disabledIconColor);
     method @androidx.compose.runtime.Composable public androidx.wear.compose.material3.ButtonColors filledTonalButtonColors();
     method @androidx.compose.runtime.Composable public androidx.wear.compose.material3.ButtonColors filledTonalButtonColors(optional long containerColor, optional long contentColor, optional long secondaryContentColor, optional long iconColor, optional long disabledContainerColor, optional long disabledContentColor, optional long disabledSecondaryContentColor, optional long disabledIconColor);
     method public float getButtonHorizontalPadding();
diff --git a/wear/compose/compose-material3/api/restricted_current.txt b/wear/compose/compose-material3/api/restricted_current.txt
index f4f6ce2..d480645 100644
--- a/wear/compose/compose-material3/api/restricted_current.txt
+++ b/wear/compose/compose-material3/api/restricted_current.txt
@@ -27,8 +27,6 @@
     method @androidx.compose.runtime.Composable public androidx.wear.compose.material3.ButtonColors buttonColors(optional long containerColor, optional long contentColor, optional long secondaryContentColor, optional long iconColor, optional long disabledContainerColor, optional long disabledContentColor, optional long disabledSecondaryContentColor, optional long disabledIconColor);
     method @androidx.compose.runtime.Composable public androidx.wear.compose.material3.ButtonColors childButtonColors();
     method @androidx.compose.runtime.Composable public androidx.wear.compose.material3.ButtonColors childButtonColors(optional long contentColor, optional long secondaryContentColor, optional long iconColor, optional long disabledContentColor, optional long disabledSecondaryContentColor, optional long disabledIconColor);
-    method @androidx.compose.runtime.Composable public androidx.wear.compose.material3.ButtonColors filledButtonColors();
-    method @androidx.compose.runtime.Composable public androidx.wear.compose.material3.ButtonColors filledButtonColors(optional long containerColor, optional long contentColor, optional long secondaryContentColor, optional long iconColor, optional long disabledContainerColor, optional long disabledContentColor, optional long disabledSecondaryContentColor, optional long disabledIconColor);
     method @androidx.compose.runtime.Composable public androidx.wear.compose.material3.ButtonColors filledTonalButtonColors();
     method @androidx.compose.runtime.Composable public androidx.wear.compose.material3.ButtonColors filledTonalButtonColors(optional long containerColor, optional long contentColor, optional long secondaryContentColor, optional long iconColor, optional long disabledContainerColor, optional long disabledContentColor, optional long disabledSecondaryContentColor, optional long disabledIconColor);
     method public float getButtonHorizontalPadding();
diff --git a/wear/compose/compose-material3/integration-tests/src/main/java/androidx/wear/compose/material3/demos/ButtonDemo.kt b/wear/compose/compose-material3/integration-tests/src/main/java/androidx/wear/compose/material3/demos/ButtonDemo.kt
index b7d60ac..fa15794 100644
--- a/wear/compose/compose-material3/integration-tests/src/main/java/androidx/wear/compose/material3/demos/ButtonDemo.kt
+++ b/wear/compose/compose-material3/integration-tests/src/main/java/androidx/wear/compose/material3/demos/ButtonDemo.kt
@@ -435,7 +435,7 @@
 @Composable
 private fun MultilineButton(
     enabled: Boolean,
-    colors: ButtonColors = ButtonDefaults.filledButtonColors(),
+    colors: ButtonColors = ButtonDefaults.buttonColors(),
     icon: (@Composable BoxScope.() -> Unit)? = null,
     label: @Composable RowScope.() -> Unit = {
         Text(
@@ -457,7 +457,7 @@
 @Composable
 private fun Multiline3SlotButton(
     enabled: Boolean,
-    colors: ButtonColors = ButtonDefaults.filledButtonColors(),
+    colors: ButtonColors = ButtonDefaults.buttonColors(),
     icon: (@Composable BoxScope.() -> Unit)? = null,
     label: @Composable RowScope.() -> Unit = {
         Text(
diff --git a/wear/compose/compose-material3/src/androidTest/kotlin/androidx/wear/compose/material3/ButtonTest.kt b/wear/compose/compose-material3/src/androidTest/kotlin/androidx/wear/compose/material3/ButtonTest.kt
index 5042316..ed0430c 100644
--- a/wear/compose/compose-material3/src/androidTest/kotlin/androidx/wear/compose/material3/ButtonTest.kt
+++ b/wear/compose/compose-material3/src/androidTest/kotlin/androidx/wear/compose/material3/ButtonTest.kt
@@ -474,7 +474,7 @@
     fun gives_enabled_three_slot_button_correct_colors() {
         rule.verifyThreeSlotButtonColors(
             status = Status.Enabled,
-            expectedColor = { ButtonDefaults.filledButtonColors() },
+            expectedColor = { ButtonDefaults.buttonColors() },
             content = { ThreeSlotFilledButton(Status.Enabled) }
         )
     }
@@ -484,7 +484,7 @@
     fun gives_disabled_three_slot_button_correct_colors() {
         rule.verifyThreeSlotButtonColors(
             status = Status.Disabled,
-            expectedColor = { ButtonDefaults.filledButtonColors() },
+            expectedColor = { ButtonDefaults.buttonColors() },
             content = { ThreeSlotFilledButton(Status.Disabled) }
         )
     }
@@ -756,7 +756,7 @@
     fun gives_enabled_compact_button_correct_colors() {
         rule.verifyCompactButtonColors(
             status = Status.Enabled,
-            colors = { ButtonDefaults.filledButtonColors() }
+            colors = { ButtonDefaults.buttonColors() }
         )
     }
 
@@ -765,7 +765,7 @@
     fun gives_disabled_compact_button_correct_colors() {
         rule.verifyCompactButtonColors(
             status = Status.Disabled,
-            colors = { ButtonDefaults.filledButtonColors() }
+            colors = { ButtonDefaults.buttonColors() }
         )
     }
 
diff --git a/wear/compose/compose-material3/src/main/java/androidx/wear/compose/material3/Button.kt b/wear/compose/compose-material3/src/main/java/androidx/wear/compose/material3/Button.kt
index 695c72e..daf7691 100644
--- a/wear/compose/compose-material3/src/main/java/androidx/wear/compose/material3/Button.kt
+++ b/wear/compose/compose-material3/src/main/java/androidx/wear/compose/material3/Button.kt
@@ -73,7 +73,7 @@
  * extend to a maximum of 3 lines in which case, the [Button] height adjusts to accommodate the
  * contents.
  *
- * [Button] takes the [ButtonDefaults.filledButtonColors] color scheme by default,
+ * [Button] takes the [ButtonDefaults.buttonColors] color scheme by default,
  * with colored background, contrasting content color and no border. This is a high-emphasis button
  * for the primary, most important or most common action on a screen.
  *
@@ -97,7 +97,7 @@
  * @param shape Defines the button's shape. It is strongly recommended to use the default as this
  * shape is a key characteristic of the Wear Material3 Theme
  * @param colors [ButtonColors] that will be used to resolve the background and content color for
- * this button in different states. See [ButtonDefaults.filledButtonColors].
+ * this button in different states. See [ButtonDefaults.buttonColors].
  * @param border Optional [BorderStroke] that will be used to resolve the border for this
  * button in different states.
  * @param contentPadding The spacing values to apply internally between the container and the
@@ -113,7 +113,7 @@
     modifier: Modifier = Modifier,
     enabled: Boolean = true,
     shape: Shape = FilledButtonTokens.ContainerShape.value,
-    colors: ButtonColors = ButtonDefaults.filledButtonColors(),
+    colors: ButtonColors = ButtonDefaults.buttonColors(),
     border: BorderStroke? = null,
     contentPadding: PaddingValues = ButtonDefaults.ContentPadding,
     interactionSource: MutableInteractionSource? = null,
@@ -148,7 +148,7 @@
  * unblocking actions in a flow with less emphasis than [Button].
  *
  * Other recommended buttons with [ButtonColors] for different levels of emphasis are:
- * [Button] which defaults to [ButtonDefaults.filledButtonColors],
+ * [Button] which defaults to [ButtonDefaults.buttonColors],
  * [OutlinedButton] which defaults to [ButtonDefaults.outlinedButtonColors] and
  * [ChildButton] which defaults to [ButtonDefaults.childButtonColors].
  * Buttons can also take an image background using [ButtonDefaults.imageBackgroundButtonColors].
@@ -217,7 +217,7 @@
  * for important, non-primary actions that need attention.
  *
  * Other recommended buttons with [ButtonColors] for different levels of emphasis are:
- * [Button] which defaults to [ButtonDefaults.filledButtonColors],
+ * [Button] which defaults to [ButtonDefaults.buttonColors],
  * [FilledTonalButton] which defaults to [ButtonDefaults.filledTonalButtonColors],
  * [ChildButton] which defaults to [ButtonDefaults.childButtonColors].
  * Buttons can also take an image background using [ButtonDefaults.imageBackgroundButtonColors].
@@ -286,7 +286,7 @@
  * or supplementary actions with the least amount of prominence.
  *
  * Other recommended buttons with [ButtonColors] for different levels of emphasis are:
- * [Button] which defaults to [ButtonDefaults.filledButtonColors],
+ * [Button] which defaults to [ButtonDefaults.buttonColors],
  * [FilledTonalButton] which defaults to [ButtonDefaults.filledTonalButtonColors],
  * [OutlinedButton] which defaults to [ButtonDefaults.outlinedButtonColors] and
  * Buttons can also take an image background using [ButtonDefaults.imageBackgroundButtonColors].
@@ -354,7 +354,7 @@
  * If a icon is provided then the labels should be "start" aligned, e.g. left aligned in ltr so that
  * the text starts next to the icon.
  *
- * [Button] takes the [ButtonDefaults.filledButtonColors] color scheme by default,
+ * [Button] takes the [ButtonDefaults.buttonColors] color scheme by default,
  * with colored background, contrasting content color and no border. This is a high-emphasis button
  * for the primary, most important or most common action on a screen.
  *
@@ -386,7 +386,7 @@
  * shape is a key characteristic of the Wear Material3 Theme
  * @param colors [ButtonColors] that will be used to resolve the background and content color for
  * this button in different states. See [ButtonDefaults.buttonColors]. Defaults to
- * [ButtonDefaults.filledButtonColors]
+ * [ButtonDefaults.buttonColors]
  * @param border Optional [BorderStroke] that will be used to resolve the button border in
  * different states.
  * @param contentPadding The spacing values to apply internally between the container and the
@@ -406,7 +406,7 @@
     icon: (@Composable BoxScope.() -> Unit)? = null,
     enabled: Boolean = true,
     shape: Shape = FilledButtonTokens.ContainerShape.value,
-    colors: ButtonColors = ButtonDefaults.filledButtonColors(),
+    colors: ButtonColors = ButtonDefaults.buttonColors(),
     border: BorderStroke? = null,
     contentPadding: PaddingValues = ButtonDefaults.ContentPadding,
     interactionSource: MutableInteractionSource? = null,
@@ -451,7 +451,7 @@
  * unblocking actions in a flow with less emphasis than [Button].
  *
  * Other recommended buttons with [ButtonColors] for different levels of emphasis are:
- * [Button] which defaults to [ButtonDefaults.filledButtonColors],
+ * [Button] which defaults to [ButtonDefaults.buttonColors],
  * [OutlinedButton] which defaults to [ButtonDefaults.outlinedButtonColors] and
  * [ChildButton] which defaults to [ButtonDefaults.childButtonColors].
  * Buttons can also take an image background using [ButtonDefaults.imageBackgroundButtonColors].
@@ -539,7 +539,7 @@
  * for important, non-primary actions that need attention.
  *
  * Other recommended buttons with [ButtonColors] for different levels of emphasis are:
- * [Button] which defaults to [ButtonDefaults.filledButtonColors],
+ * [Button] which defaults to [ButtonDefaults.buttonColors],
  * [FilledTonalButton] which defaults to [ButtonDefaults.filledTonalButtonColors],
  * [ChildButton] which defaults to [ButtonDefaults.childButtonColors].
  * Buttons can also take an image background using [ButtonDefaults.imageBackgroundButtonColors].
@@ -625,7 +625,7 @@
  * or supplementary actions with the least amount of prominence.
  *
  * Other recommended buttons with [ButtonColors] for different levels of emphasis are:
- * [Button] which defaults to [ButtonDefaults.filledButtonColors],
+ * [Button] which defaults to [ButtonDefaults.buttonColors],
  * [FilledTonalButton] which defaults to [ButtonDefaults.filledTonalButtonColors],
  * [OutlinedButton] which defaults to [ButtonDefaults.outlinedButtonColors].
  * Buttons can also take an image background using [ButtonDefaults.imageBackgroundButtonColors].
@@ -719,7 +719,7 @@
  * If neither icon nor label is provided then the button will displayed like an icon only button but
  * with no contents or background color.
  *
- * [CompactButton] takes the [ButtonDefaults.filledButtonColors] color scheme by default,
+ * [CompactButton] takes the [ButtonDefaults.buttonColors] color scheme by default,
  * with colored background, contrasting content color and no border. This is a high-emphasis button
  * for the primary, most important or most common action on a screen.
  *
@@ -752,7 +752,7 @@
  * horizontally and vertically aligned icon of size [ButtonDefaults.SmallIconSize] when used
  * with a label or [ButtonDefaults.IconSize] when used as the only content in the button.
  * @param colors [ButtonColors] that will be used to resolve the background and content color for
- * this button in different states. See [ButtonDefaults.filledButtonColors].
+ * this button in different states. See [ButtonDefaults.buttonColors].
  * @param enabled Controls the enabled state of the button. When `false`, this button will not
  * be clickable
  * @param interactionSource an optional hoisted [MutableInteractionSource] for observing and
@@ -773,7 +773,7 @@
     icon: (@Composable BoxScope.() -> Unit)? = null,
     enabled: Boolean = true,
     shape: Shape = CompactButtonTokens.ContainerShape.value,
-    colors: ButtonColors = ButtonDefaults.filledButtonColors(),
+    colors: ButtonColors = ButtonDefaults.buttonColors(),
     border: BorderStroke? = null,
     contentPadding: PaddingValues = ButtonDefaults.CompactButtonContentPadding,
     interactionSource: MutableInteractionSource? = null,
@@ -829,63 +829,13 @@
  * Contains the default values used by [Button]
  */
 object ButtonDefaults {
-   /**
-    * Creates a [ButtonColors] with colored background and contrasting content color,
-    * the defaults for high emphasis buttons like [Button], for the primary, most important
-    * or most common action on a screen.
-    *
-    * If a button is disabled then the content will have an alpha([DisabledContentAlpha]) value
-    * applied and container will have an alpha([DisabledContainerAlpha]) value applied.
-    */
-    @Composable
-    fun filledButtonColors() = MaterialTheme.colorScheme.defaultFilledButtonColors
-
-    /**
-     * Creates a [ButtonColors] with colored background and contrasting content color,
-     * the defaults for high emphasis buttons like [Button], for the primary, most important
-     * or most common action on a screen.
-     *
-     * If a button is disabled then the content will have an alpha([DisabledContentAlpha]) value
-     * applied and container will have an alpha([DisabledContainerAlpha]) value applied.
-     *
-     * @param containerColor The background color of this [Button] when enabled
-     * @param contentColor The content color of this [Button] when enabled
-     * @param secondaryContentColor The secondary content color of this [Button] when enabled, used
-     * for secondaryLabel content
-     * @param iconColor The icon color of this [Button] when enabled, used for icon content
-     * @param disabledContainerColor The background color of this [Button] when not enabled
-     * @param disabledContentColor The content color of this [Button] when not enabled
-     * @param disabledSecondaryContentColor The secondary content color of this [Button] when not
-     * enabled
-     * @param disabledIconColor The content color of this [Button] when not enabled
-     */
-    @Composable
-    fun filledButtonColors(
-        containerColor: Color = Color.Unspecified,
-        contentColor: Color = Color.Unspecified,
-        secondaryContentColor: Color = Color.Unspecified,
-        iconColor: Color = Color.Unspecified,
-        disabledContainerColor: Color = Color.Unspecified,
-        disabledContentColor: Color = Color.Unspecified,
-        disabledSecondaryContentColor: Color = Color.Unspecified,
-        disabledIconColor: Color = Color.Unspecified
-    ): ButtonColors = MaterialTheme.colorScheme.defaultFilledButtonColors.copy(
-        containerColor = containerColor,
-        contentColor = contentColor,
-        secondaryContentColor = secondaryContentColor,
-        iconColor = iconColor,
-        disabledContainerColor = disabledContainerColor,
-        disabledContentColor = disabledContentColor,
-        disabledSecondaryContentColor = disabledSecondaryContentColor,
-        disabledIconColor = disabledIconColor
-    )
 
     /**
      * Creates a [ButtonColors] with a muted background and contrasting content color,
      * the defaults for medium emphasis buttons like [FilledTonalButton].
      * Use [filledTonalButtonColors] for important actions that don't distract from
      * other onscreen elements, such as final or unblocking actions in a flow with less emphasis
-     * than [filledButtonColors].
+     * than [buttonColors].
      *
      * If a button is disabled then the content will have an alpha([DisabledContentAlpha])
      * value applied and container will have alpha ([DisabledContainerAlpha]) value applied.
@@ -898,7 +848,7 @@
      * the defaults for medium emphasis buttons like [FilledTonalButton].
      * Use [filledTonalButtonColors] for important actions that don't distract from
      * other onscreen elements, such as final or unblocking actions in a flow with less emphasis
-     * than [filledButtonColors].
+     * than [buttonColors].
      *
      * If a button is disabled then the content will have an alpha([DisabledContentAlpha])
      * value applied and container will have alpha ([DisabledContainerAlpha]) value applied.
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 6f1c147..cf82fd0 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
@@ -83,7 +83,7 @@
     interactionSource: MutableInteractionSource? = null,
     content: @Composable BoxScope.() -> Unit,
 ) {
-    androidx.wear.compose.materialcore.Button(
+    androidx.wear.compose.materialcore.RoundButton(
         onClick = onClick,
         modifier.minimumInteractiveComponentSize(),
         enabled = enabled,
diff --git a/wear/compose/compose-material3/src/main/java/androidx/wear/compose/material3/TextButton.kt b/wear/compose/compose-material3/src/main/java/androidx/wear/compose/material3/TextButton.kt
index 50afcca..9a3571b 100644
--- a/wear/compose/compose-material3/src/main/java/androidx/wear/compose/material3/TextButton.kt
+++ b/wear/compose/compose-material3/src/main/java/androidx/wear/compose/material3/TextButton.kt
@@ -89,7 +89,7 @@
     interactionSource: MutableInteractionSource? = null,
     content: @Composable BoxScope.() -> Unit,
 ) {
-    androidx.wear.compose.materialcore.Button(
+    androidx.wear.compose.materialcore.RoundButton(
         onClick = onClick,
         modifier.minimumInteractiveComponentSize(),
         enabled = enabled,
diff --git a/wear/compose/compose-navigation/build.gradle b/wear/compose/compose-navigation/build.gradle
index 02f0362..3f37907 100644
--- a/wear/compose/compose-navigation/build.gradle
+++ b/wear/compose/compose-navigation/build.gradle
@@ -37,7 +37,7 @@
     api("androidx.navigation:navigation-runtime:2.6.0")
     api(project(":wear:compose:compose-material"))
     api("androidx.activity:activity-compose:1.7.0")
-    api("androidx.lifecycle:lifecycle-viewmodel-compose:2.6.0")
+    api("androidx.lifecycle:lifecycle-viewmodel-compose:2.7.0")
 
     implementation(libs.kotlinStdlib)
     implementation("androidx.navigation:navigation-common:2.6.0")
@@ -51,7 +51,7 @@
     androidTestImplementation(project(":wear:compose:compose-material"))
     androidTestImplementation(project(":wear:compose:compose-navigation-samples"))
     androidTestImplementation(libs.truth)
-    androidTestImplementation("androidx.lifecycle:lifecycle-runtime-testing:2.6.0")
+    androidTestImplementation("androidx.lifecycle:lifecycle-runtime-testing:2.7.0")
     androidTestImplementation("androidx.navigation:navigation-testing:2.6.0")
 }
 
diff --git a/wear/compose/integration-tests/demos/build.gradle b/wear/compose/integration-tests/demos/build.gradle
index 24f6946..c2f20c3 100644
--- a/wear/compose/integration-tests/demos/build.gradle
+++ b/wear/compose/integration-tests/demos/build.gradle
@@ -69,9 +69,9 @@
     androidTestImplementation(project(":activity:activity-compose"))
     androidTestImplementation(project(":activity:activity-ktx"))
     androidTestImplementation(project(":compose:ui:ui-test-junit4"))
-    androidTestImplementation(project(":lifecycle:lifecycle-runtime"))
-    androidTestImplementation(project(":lifecycle:lifecycle-common"))
-    androidTestImplementation(project(":lifecycle:lifecycle-runtime-ktx"))
+    androidTestImplementation("androidx.lifecycle:lifecycle-runtime:2.7.0")
+    androidTestImplementation("androidx.lifecycle:lifecycle-common:2.7.0")
+    androidTestImplementation("androidx.lifecycle:lifecycle-runtime-ktx:2.7.0")
     androidTestImplementation(libs.testCore)
     androidTestImplementation(libs.testRules)
     androidTestImplementation(libs.testRunner)
diff --git a/wear/compose/integration-tests/demos/src/main/java/androidx/wear/compose/integration/demos/MaterialDemos.kt b/wear/compose/integration-tests/demos/src/main/java/androidx/wear/compose/integration/demos/MaterialDemos.kt
index ba48fd0..d102123 100644
--- a/wear/compose/integration-tests/demos/src/main/java/androidx/wear/compose/integration/demos/MaterialDemos.kt
+++ b/wear/compose/integration-tests/demos/src/main/java/androidx/wear/compose/integration/demos/MaterialDemos.kt
@@ -30,6 +30,9 @@
 import androidx.wear.compose.integration.demos.common.Centralize
 import androidx.wear.compose.integration.demos.common.ComposableDemo
 import androidx.wear.compose.integration.demos.common.DemoCategory
+import androidx.wear.compose.material.ListHeader
+import androidx.wear.compose.material.MaterialTheme
+import androidx.wear.compose.material.Text
 import androidx.wear.compose.material.samples.AlertDialogSample
 import androidx.wear.compose.material.samples.AlertWithButtons
 import androidx.wear.compose.material.samples.AlertWithChips
@@ -675,5 +678,28 @@
             )
         ),
         ComposableDemo("Settings Demo") { SettingsDemo() },
+        DemoCategory(
+            "ListHeader",
+            listOf(
+                ComposableDemo("Sample") {
+                    Centralize {
+                        ListHeader {
+                            Text("Header", maxLines = 3)
+                        }
+                    }
+                },
+                ComposableDemo("MultiLine Sample") {
+                    Centralize {
+                        ListHeader {
+                            Text(
+                                text = "ListHeader that spans multiple lines in a large " +
+                                    "font and should expand to fit the contents",
+                                style = MaterialTheme.typography.title3
+                            )
+                        }
+                    }
+                }
+            )
+        )
     ),
 )
diff --git a/wear/compose/integration-tests/navigation/build.gradle b/wear/compose/integration-tests/navigation/build.gradle
index dc4e776..46a2548 100644
--- a/wear/compose/integration-tests/navigation/build.gradle
+++ b/wear/compose/integration-tests/navigation/build.gradle
@@ -53,7 +53,7 @@
     implementation(project(":wear:compose:compose-material-samples"))
     implementation(project(':wear:compose:compose-navigation'))
 
-    androidTestImplementation(project(":lifecycle:lifecycle-common"))
+    androidTestImplementation("androidx.lifecycle:lifecycle-common:2.7.0")
     // Uses project dependency to match collections/compose-runtime
     androidTestImplementation api("androidx.annotation:annotation:1.7.0")
 }
\ No newline at end of file
diff --git a/wear/protolayout/protolayout-expression-pipeline/build.gradle b/wear/protolayout/protolayout-expression-pipeline/build.gradle
index 7e20791..e3b079e 100644
--- a/wear/protolayout/protolayout-expression-pipeline/build.gradle
+++ b/wear/protolayout/protolayout-expression-pipeline/build.gradle
@@ -35,7 +35,7 @@
     implementation("androidx.collection:collection:1.2.0")
     implementation("androidx.core:core:1.7.0")
     implementation("androidx.concurrent:concurrent-futures:1.1.0")
-    implementation("androidx.annotation:annotation-experimental:1.4.0-rc01")
+    implementation("androidx.annotation:annotation-experimental:1.4.0")
     implementation(project(path: ":wear:protolayout:protolayout-proto", configuration: "shadow"))
     implementation(project(":wear:protolayout:protolayout-expression"))
 
diff --git a/wear/protolayout/protolayout-expression/build.gradle b/wear/protolayout/protolayout-expression/build.gradle
index 312cabf..ed1d2f6 100644
--- a/wear/protolayout/protolayout-expression/build.gradle
+++ b/wear/protolayout/protolayout-expression/build.gradle
@@ -32,7 +32,7 @@
     annotationProcessor(libs.nullaway)
     api("androidx.annotation:annotation:1.2.0")
 
-    implementation("androidx.annotation:annotation-experimental:1.4.0-rc01")
+    implementation("androidx.annotation:annotation-experimental:1.4.0")
     implementation("androidx.collection:collection:1.2.0")
     implementation(project(path: ":wear:protolayout:protolayout-proto", configuration: "shadow"))
 
diff --git a/wear/protolayout/protolayout-material/api/current.txt b/wear/protolayout/protolayout-material/api/current.txt
index 01b19f2..7e89155 100644
--- a/wear/protolayout/protolayout-material/api/current.txt
+++ b/wear/protolayout/protolayout-material/api/current.txt
@@ -107,6 +107,7 @@
     method public androidx.wear.protolayout.DimensionBuilders.DegreesProp getProgress();
     method public androidx.wear.protolayout.DimensionBuilders.DegreesProp getStartAngle();
     method public androidx.wear.protolayout.DimensionBuilders.DpProp getStrokeWidth();
+    method public boolean isOuterMarginApplied();
   }
 
   public static final class CircularProgressIndicator.Builder {
@@ -116,6 +117,7 @@
     method public androidx.wear.protolayout.material.CircularProgressIndicator.Builder setContentDescription(androidx.wear.protolayout.TypeBuilders.StringProp);
     method public androidx.wear.protolayout.material.CircularProgressIndicator.Builder setContentDescription(CharSequence);
     method public androidx.wear.protolayout.material.CircularProgressIndicator.Builder setEndAngle(float);
+    method public androidx.wear.protolayout.material.CircularProgressIndicator.Builder setOuterMarginApplied(boolean);
     method public androidx.wear.protolayout.material.CircularProgressIndicator.Builder setProgress(androidx.wear.protolayout.TypeBuilders.FloatProp);
     method public androidx.wear.protolayout.material.CircularProgressIndicator.Builder setProgress(@FloatRange(from=0, to=1) float);
     method public androidx.wear.protolayout.material.CircularProgressIndicator.Builder setStartAngle(float);
diff --git a/wear/protolayout/protolayout-material/api/restricted_current.txt b/wear/protolayout/protolayout-material/api/restricted_current.txt
index 01b19f2..7e89155 100644
--- a/wear/protolayout/protolayout-material/api/restricted_current.txt
+++ b/wear/protolayout/protolayout-material/api/restricted_current.txt
@@ -107,6 +107,7 @@
     method public androidx.wear.protolayout.DimensionBuilders.DegreesProp getProgress();
     method public androidx.wear.protolayout.DimensionBuilders.DegreesProp getStartAngle();
     method public androidx.wear.protolayout.DimensionBuilders.DpProp getStrokeWidth();
+    method public boolean isOuterMarginApplied();
   }
 
   public static final class CircularProgressIndicator.Builder {
@@ -116,6 +117,7 @@
     method public androidx.wear.protolayout.material.CircularProgressIndicator.Builder setContentDescription(androidx.wear.protolayout.TypeBuilders.StringProp);
     method public androidx.wear.protolayout.material.CircularProgressIndicator.Builder setContentDescription(CharSequence);
     method public androidx.wear.protolayout.material.CircularProgressIndicator.Builder setEndAngle(float);
+    method public androidx.wear.protolayout.material.CircularProgressIndicator.Builder setOuterMarginApplied(boolean);
     method public androidx.wear.protolayout.material.CircularProgressIndicator.Builder setProgress(androidx.wear.protolayout.TypeBuilders.FloatProp);
     method public androidx.wear.protolayout.material.CircularProgressIndicator.Builder setProgress(@FloatRange(from=0, to=1) float);
     method public androidx.wear.protolayout.material.CircularProgressIndicator.Builder setStartAngle(float);
diff --git a/wear/protolayout/protolayout-material/build.gradle b/wear/protolayout/protolayout-material/build.gradle
index d2acf88..8215bb4 100644
--- a/wear/protolayout/protolayout-material/build.gradle
+++ b/wear/protolayout/protolayout-material/build.gradle
@@ -36,7 +36,7 @@
 
     implementation(project(":wear:protolayout:protolayout-material-core"))
     implementation(project(path: ":wear:protolayout:protolayout-proto", configuration: "shadow"))
-    implementation("androidx.annotation:annotation-experimental:1.4.0-rc01")
+    implementation("androidx.annotation:annotation-experimental:1.4.0")
 
 
     androidTestImplementation(libs.junit)
diff --git a/wear/protolayout/protolayout-material/src/androidTest/java/androidx/wear/protolayout/material/TestCasesGenerator.java b/wear/protolayout/protolayout-material/src/androidTest/java/androidx/wear/protolayout/material/TestCasesGenerator.java
index 6c6c8d6..1d7ce0b 100644
--- a/wear/protolayout/protolayout-material/src/androidTest/java/androidx/wear/protolayout/material/TestCasesGenerator.java
+++ b/wear/protolayout/protolayout-material/src/androidTest/java/androidx/wear/protolayout/material/TestCasesGenerator.java
@@ -312,6 +312,26 @@
                                 new ProgressIndicatorColors(Color.BLUE, Color.YELLOW))
                         .build());
         testCases.put(
+                "circularprogressindicator_in_smaller_box_with_margins_full_90",
+                new Box.Builder()
+                        .setWidth(dp(50))
+                        .setHeight(dp(50))
+                        .addContent(
+                                new CircularProgressIndicator.Builder().setProgress(0.25f).build())
+                        .build());
+        testCases.put(
+                "circularprogressindicator_in_smaller_box_without_margins_full_90",
+                new Box.Builder()
+                        .setWidth(dp(50))
+                        .setHeight(dp(50))
+                        .addContent(
+                                new CircularProgressIndicator.Builder()
+                                        .setProgress(0.25f)
+                                        .setOuterMarginApplied(false)
+                                        .build())
+                        .build());
+
+        testCases.put(
                 "default_text_golden" + goldenSuffix, new Text.Builder(context, "Testing").build());
         testCases.put(
                 "not_scaled_text_golden" + NORMAL_SCALE_SUFFIX,
diff --git a/wear/protolayout/protolayout-material/src/main/java/androidx/wear/protolayout/material/CircularProgressIndicator.java b/wear/protolayout/protolayout-material/src/main/java/androidx/wear/protolayout/material/CircularProgressIndicator.java
index 76ef0f5..f0865e4 100644
--- a/wear/protolayout/protolayout-material/src/main/java/androidx/wear/protolayout/material/CircularProgressIndicator.java
+++ b/wear/protolayout/protolayout-material/src/main/java/androidx/wear/protolayout/material/CircularProgressIndicator.java
@@ -117,6 +117,7 @@
         @NonNull private DegreesProp mStartAngle = degrees(DEFAULT_START_ANGLE);
         @NonNull private DegreesProp mEndAngle = degrees(DEFAULT_END_ANGLE);
         @NonNull private FloatProp mProgress = staticFloat(0f);
+        private boolean mIsMarginApplied = true;
 
         /** Creates a builder for the {@link CircularProgressIndicator}. */
         public Builder() {}
@@ -231,6 +232,24 @@
         }
 
         /**
+         * Sets whether this {@link CircularProgressIndicator} should have outer margin or not.
+         *
+         * <p>If this indicator is used as a smaller element, use this method to remove an
+         * additional margin around it by setting it to {@code false}.
+         *
+         * <p>Otherwise, if this indicator is used as a full screen one or in {@link
+         * androidx.wear.protolayout.material.layouts.EdgeContentLayout2}, it's strongly recommended
+         * to set this to {@code true}.
+         *
+         * <p>If not set, defaults to true.
+         */
+        @NonNull
+        public Builder setOuterMarginApplied(boolean isApplied) {
+            this.mIsMarginApplied = isApplied;
+            return this;
+        }
+
+        /**
          * Constructs and returns {@link CircularProgressIndicator} with the provided field and
          * look.
          */
@@ -242,16 +261,16 @@
             DegreesProp length = getLength();
             Modifiers.Builder modifiers =
                     new Modifiers.Builder()
-                            .setPadding(
-                                    new Padding.Builder()
-                                            .setRtlAware(true)
-                                            .setAll(DEFAULT_PADDING)
-                                            .build())
                             .setMetadata(
                                     new ElementMetadata.Builder()
                                             .setTagData(getTagBytes(METADATA_TAG))
                                             .build());
 
+            if (mIsMarginApplied) {
+                modifiers.setPadding(
+                        new Padding.Builder().setRtlAware(true).setAll(DEFAULT_PADDING).build());
+            }
+
             if (mContentDescription != null) {
                 modifiers.setSemantics(
                         new Semantics.Builder().setContentDescription(mContentDescription).build());
@@ -385,6 +404,12 @@
                 checkNotNull(checkNotNull(mElement.getModifiers()).getMetadata()));
     }
 
+    /** Returns whether there is a margin around this indicator or not. */
+    public boolean isOuterMarginApplied() {
+        return this.mElement.getModifiers() != null
+                && this.mElement.getModifiers().getPadding() != null;
+    }
+
     /**
      * Returns CircularProgressIndicator object from the given LayoutElement (e.g. one retrieved
      * from a container's content with {@code container.getContents().get(index)}) if that element
diff --git a/wear/protolayout/protolayout-material/src/test/java/androidx/wear/protolayout/material/CircularProgressIndicatorTest.java b/wear/protolayout/protolayout-material/src/test/java/androidx/wear/protolayout/material/CircularProgressIndicatorTest.java
index 45c7f18..ce4cd40 100644
--- a/wear/protolayout/protolayout-material/src/test/java/androidx/wear/protolayout/material/CircularProgressIndicatorTest.java
+++ b/wear/protolayout/protolayout-material/src/test/java/androidx/wear/protolayout/material/CircularProgressIndicatorTest.java
@@ -97,6 +97,7 @@
                         .setCircularProgressIndicatorColors(colors)
                         .setStrokeWidth(thickness)
                         .setContentDescription(contentDescription)
+                        .setOuterMarginApplied(false)
                         .build();
 
         assertProgressIndicator(
@@ -107,6 +108,7 @@
                 colors,
                 thickness,
                 contentDescription);
+        assertThat(circularProgressIndicator.isOuterMarginApplied()).isFalse();
     }
 
     @Test
diff --git a/wear/protolayout/protolayout/build.gradle b/wear/protolayout/protolayout/build.gradle
index 2515546..1cae83e2 100644
--- a/wear/protolayout/protolayout/build.gradle
+++ b/wear/protolayout/protolayout/build.gradle
@@ -33,7 +33,7 @@
     api("androidx.annotation:annotation:1.2.0")
     api(project(":wear:protolayout:protolayout-expression"))
 
-    implementation("androidx.annotation:annotation-experimental:1.4.0-rc01")
+    implementation("androidx.annotation:annotation-experimental:1.4.0")
     implementation(project(path: ":wear:protolayout:protolayout-proto", configuration: "shadow"))
 
     lintChecks(project(":wear:protolayout:protolayout-lint"))
diff --git a/wear/tiles/tiles/build.gradle b/wear/tiles/tiles/build.gradle
index f10a1a8..cb9890d 100644
--- a/wear/tiles/tiles/build.gradle
+++ b/wear/tiles/tiles/build.gradle
@@ -35,7 +35,7 @@
     api(project(":wear:protolayout:protolayout-expression"))
     api(libs.guavaListenableFuture)
 
-    implementation("androidx.annotation:annotation-experimental:1.4.0-rc01")
+    implementation("androidx.annotation:annotation-experimental:1.4.0")
     implementation("androidx.concurrent:concurrent-futures:1.1.0")
     implementation(project(path: ":wear:tiles:tiles-proto"))
 
diff --git a/wear/watchface/watchface-complications/src/main/java/androidx/wear/watchface/complications/SystemDataSources.kt b/wear/watchface/watchface-complications/src/main/java/androidx/wear/watchface/complications/SystemDataSources.kt
index e0c1ef1..3717584 100644
--- a/wear/watchface/watchface-complications/src/main/java/androidx/wear/watchface/complications/SystemDataSources.kt
+++ b/wear/watchface/watchface-complications/src/main/java/androidx/wear/watchface/complications/SystemDataSources.kt
@@ -75,8 +75,9 @@
          * RECEIVE_COMPLICATION_DATA permission has been granted.
          *
          * This complication data source is only guaranteed to support [ComplicationType.SHORT_TEXT]
-         * although it's a good idea for the slot to support [ComplicationType.SMALL_IMAGE] too
-         * since OEMs may choose to serve a shortcut to their health app instead of the live value.
+         * and [ComplicationType.GOAL_PROGRESS], although it's a good idea for the slot to support
+         * [ComplicationType.SMALL_IMAGE] too since OEMs may choose to serve a shortcut to their
+         * health app instead of the live value.
          */
         public const val DATA_SOURCE_STEP_COUNT: Int = 4
 
@@ -183,8 +184,7 @@
         public const val DATA_SOURCE_DAY_AND_DATE: Int = 16
 
         /**
-         * Id for the 'heart rate' complication data source. Note implementations are free to return
-         * a
+         * Id for the 'heart rate' complication data source.
          *
          * This complication data source is only guaranteed to support [ComplicationType.SHORT_TEXT]
          * although it's a good idea for the slot to support [ComplicationType.SMALL_IMAGE] too
diff --git a/wear/wear/src/androidTest/java/androidx/wear/widget/WearableRecyclerViewTest.java b/wear/wear/src/androidTest/java/androidx/wear/widget/WearableRecyclerViewTest.java
index ad4bf84..25d8a7a 100644
--- a/wear/wear/src/androidTest/java/androidx/wear/widget/WearableRecyclerViewTest.java
+++ b/wear/wear/src/androidTest/java/androidx/wear/widget/WearableRecyclerViewTest.java
@@ -41,6 +41,7 @@
 import androidx.test.ext.junit.rules.ActivityScenarioRule;
 import androidx.test.ext.junit.runners.AndroidJUnit4;
 import androidx.test.filters.LargeTest;
+import androidx.test.filters.SdkSuppress;
 import androidx.test.platform.app.InstrumentationRegistry;
 import androidx.wear.test.R;
 import androidx.wear.widget.util.WakeLockRule;
@@ -186,6 +187,7 @@
         });
     }
 
+    @SdkSuppress(maxSdkVersion = 33) // b/322537327
     @Test
     public void testCircularScrollingGesture() throws Throwable {
         onView(withId(R.id.wrv)).perform(swipeDownFromTopRight());
diff --git a/wear/wear/src/androidTest/java/androidx/wear/widget/drawer/WearableDrawerLayoutEspressoTest.java b/wear/wear/src/androidTest/java/androidx/wear/widget/drawer/WearableDrawerLayoutEspressoTest.java
index 7327b13..da68d8f 100644
--- a/wear/wear/src/androidTest/java/androidx/wear/widget/drawer/WearableDrawerLayoutEspressoTest.java
+++ b/wear/wear/src/androidTest/java/androidx/wear/widget/drawer/WearableDrawerLayoutEspressoTest.java
@@ -57,6 +57,7 @@
 import androidx.test.espresso.util.TreeIterables;
 import androidx.test.ext.junit.runners.AndroidJUnit4;
 import androidx.test.filters.LargeTest;
+import androidx.test.filters.SdkSuppress;
 import androidx.test.rule.ActivityTestRule;
 import androidx.wear.test.R;
 import androidx.wear.widget.drawer.DrawerTestActivity.DrawerStyle;
@@ -156,6 +157,7 @@
         onView(withId(R.id.ws_nav_drawer_text)).check(matches(withText("0")));
     }
 
+    @SdkSuppress(maxSdkVersion = 33) // b/322538394
     @Test
     public void selectingNavItemChangesTextAndClosedDrawer() {
         // GIVEN an open top drawer
@@ -372,6 +374,7 @@
         onView(withId(R.id.action_drawer)).perform(swipeDown()).check(matches(isOpened(true)));
     }
 
+    @SdkSuppress(maxSdkVersion = 33) // b/322538394
     @Test
     public void actionDrawerPeekIconShouldNotBeNull() {
         // GIVEN a drawer layout with a peeking action drawer whose menu is initialized in XML
@@ -384,6 +387,7 @@
         assertNotNull(peekIconView.getDrawable());
     }
 
+    @SdkSuppress(maxSdkVersion = 33) // b/322538394
     @Test
     public void tappingActionDrawerPeekIconShouldTriggerFirstAction() {
         // GIVEN a drawer layout with a peeking action drawer, title, and mock click listener
@@ -408,6 +412,7 @@
         verify(mockClickListener).onMenuItemClick(any(MenuItem.class));
     }
 
+    @SdkSuppress(maxSdkVersion = 33) // b/322538394
     @Test
     public void tappingActionDrawerPeekIconShouldTriggerFirstActionAfterItWasOpened() {
         // GIVEN a drawer layout with an open action drawer with a title, and mock click listener
diff --git a/work/work-runtime/src/androidTest/java/androidx/work/impl/WorkerWrapperTest.java b/work/work-runtime/src/androidTest/java/androidx/work/impl/WorkerWrapperTest.java
index 122f140..4a6200c 100644
--- a/work/work-runtime/src/androidTest/java/androidx/work/impl/WorkerWrapperTest.java
+++ b/work/work-runtime/src/androidTest/java/androidx/work/impl/WorkerWrapperTest.java
@@ -319,7 +319,7 @@
             previousId = work.getStringId();
         }
         WorkerWrapper workerWrapper = createBuilder(firstWorkId).build();
-        workerWrapper.setFailedAndResolve();
+        workerWrapper.setFailedAndResolve(new ListenableWorker.Result.Failure());
         assertThat(mWorkSpecDao.getState(firstWorkId), is(FAILED));
         assertThat(mWorkSpecDao.getState(previousId), is(FAILED));
     }
diff --git a/work/work-runtime/src/main/java/androidx/work/impl/WorkerWrapper.kt b/work/work-runtime/src/main/java/androidx/work/impl/WorkerWrapper.kt
index daf7ab1..2cbec8d 100644
--- a/work/work-runtime/src/main/java/androidx/work/impl/WorkerWrapper.kt
+++ b/work/work-runtime/src/main/java/androidx/work/impl/WorkerWrapper.kt
@@ -24,6 +24,7 @@
 import androidx.work.Configuration
 import androidx.work.Data
 import androidx.work.ListenableWorker
+import androidx.work.ListenableWorker.Result.Failure
 import androidx.work.Logger
 import androidx.work.WorkInfo
 import androidx.work.WorkerExceptionInfo
@@ -65,7 +66,6 @@
     private var worker: ListenableWorker? = builder.worker
     private val workTaskExecutor: TaskExecutor = builder.workTaskExecutor
 
-    private var result = ListenableWorker.Result.failure()
     private val configuration: Configuration = builder.configuration
     private val clock: Clock = configuration.clock
     private val foregroundProcessor: ForegroundProcessor = builder.foregroundProcessor
@@ -154,7 +154,7 @@
                 inputMergerFactory.createInputMergerWithDefaultFallback(inputMergerClassName)
             if (inputMerger == null) {
                 loge(TAG) { "Could not create Input Merger ${workSpec.inputMergerClassName}" }
-                setFailedAndResolve()
+                setFailedAndResolve(Failure())
                 return
             }
             val inputs = listOf(workSpec.input) + workSpecDao.getInputsFromPrerequisites(workSpecId)
@@ -194,7 +194,7 @@
                         "Exception handler threw an exception"
                     }
                 }
-                setFailedAndResolve()
+                setFailedAndResolve(Failure())
                 return
             }
         worker.setUsed()
@@ -241,17 +241,19 @@
             // Avoid synthetic accessors.
             val workDescription = workDescription
             workerResultFuture.addListener({
+                var result: ListenableWorker.Result = Failure()
                 try {
                     // If the ListenableWorker returns a null result treat it as a failure.
-                    val result = workerResultFuture.get()
-                    if (result == null) {
+                    val futureResult = workerResultFuture.get()
+                    result = if (futureResult == null) {
                         loge(TAG) {
                             workSpec.workerClassName +
                                 " returned a null result. Treating it as a failure."
                         }
+                        Failure()
                     } else {
-                        logd(TAG) { "${workSpec.workerClassName} returned a $result." }
-                        this.result = result
+                        logd(TAG) { "${workSpec.workerClassName} returned a $futureResult." }
+                        futureResult
                     }
                 } catch (exception: InterruptedException) {
                     loge(TAG, exception) {
@@ -259,7 +261,8 @@
                     }
                     try {
                         configuration.workerExecutionExceptionHandler?.accept(
-                            WorkerExceptionInfo(workSpec.workerClassName, params, exception))
+                            WorkerExceptionInfo(workSpec.workerClassName, params, exception)
+                        )
                     } catch (exception: Exception) {
                         loge(TAG, exception) {
                             "Exception handler threw an exception"
@@ -292,7 +295,7 @@
                         }
                     }
                 } finally {
-                    onWorkFinished()
+                    onWorkFinished(result)
                 }
             }, workTaskExecutor.getSerialTaskExecutor())
         } else {
@@ -300,7 +303,7 @@
         }
     }
 
-    private fun onWorkFinished() {
+    private fun onWorkFinished(result: ListenableWorker.Result) {
         if (!tryCheckForInterruptionAndResolve()) {
             workDatabase.runInTransaction {
                 val state = workSpecDao.getState(workSpecId)
@@ -403,13 +406,13 @@
         _future.set(needsReschedule)
     }
 
-    private fun handleResult(result: ListenableWorker.Result) {
+    private fun handleResult(result: ListenableWorker.Result?) {
         if (result is ListenableWorker.Result.Success) {
             logi(TAG) { "Worker result SUCCESS for $workDescription" }
             if (workSpec.isPeriodic) {
                 resetPeriodicAndResolve()
             } else {
-                setSucceededAndResolve()
+                setSucceededAndResolve(result)
             }
         } else if (result is ListenableWorker.Result.Retry) {
             logi(TAG) { "Worker result RETRY for $workDescription" }
@@ -419,7 +422,8 @@
             if (workSpec.isPeriodic) {
                 resetPeriodicAndResolve()
             } else {
-                setFailedAndResolve()
+                // we have here either failure or null
+                setFailedAndResolve(result ?: Failure())
             }
         }
     }
@@ -437,10 +441,10 @@
     )
 
     @VisibleForTesting
-    fun setFailedAndResolve() {
+    fun setFailedAndResolve(result: ListenableWorker.Result) {
         resolve(false) {
             iterativelyFailWorkAndDependents(workSpecId)
-            val failure = result as ListenableWorker.Result.Failure
+            val failure = result as Failure
             // Update Data as necessary.
             val output = failure.outputData
             workSpecDao.resetWorkSpecNextScheduleTimeOverride(
@@ -493,7 +497,7 @@
         }
     }
 
-    private fun setSucceededAndResolve() {
+    private fun setSucceededAndResolve(result: ListenableWorker.Result) {
         resolve(false) {
             workSpecDao.setState(WorkInfo.State.SUCCEEDED, workSpecId)
             val success = result as ListenableWorker.Result.Success