Merge "Removing usages of $S, $T, $N, $L, $W and `toJavaPoet()` across all files in Room." into androidx-main
diff --git a/activity/activity-compose-lint/src/test/java/androidx/activity/compose/lint/ActivityResultLaunchDetectorTest.kt b/activity/activity-compose-lint/src/test/java/androidx/activity/compose/lint/ActivityResultLaunchDetectorTest.kt
index 5803d94..7d5fc8b 100644
--- a/activity/activity-compose-lint/src/test/java/androidx/activity/compose/lint/ActivityResultLaunchDetectorTest.kt
+++ b/activity/activity-compose-lint/src/test/java/androidx/activity/compose/lint/ActivityResultLaunchDetectorTest.kt
@@ -19,7 +19,7 @@
 package androidx.activity.compose.lint
 
 import androidx.compose.lint.test.Stubs
-import androidx.compose.lint.test.compiledStub
+import androidx.compose.lint.test.bytecodeStub
 import com.android.tools.lint.checks.infrastructure.LintDetectorTest
 import com.android.tools.lint.checks.infrastructure.TestMode
 import com.android.tools.lint.detector.api.Detector
@@ -40,7 +40,7 @@
     override fun getIssues(): MutableList<Issue> =
         mutableListOf(ActivityResultLaunchDetector.LaunchDuringComposition)
 
-    private val MANAGED_ACTIVITY_RESULT_LAUNCHER = compiledStub(
+    private val MANAGED_ACTIVITY_RESULT_LAUNCHER = bytecodeStub(
         filename = "ActivityResultRegistry.kt",
         filepath = "androidx/activity/compose",
         checksum = 0x42f3e9f,
diff --git a/activity/activity-compose/build.gradle b/activity/activity-compose/build.gradle
index 52f72760..ec9cdc0 100644
--- a/activity/activity-compose/build.gradle
+++ b/activity/activity-compose/build.gradle
@@ -30,6 +30,7 @@
     api("androidx.compose.runtime:runtime:1.0.1")
     api("androidx.compose.runtime:runtime-saveable:1.0.1")
     api(projectOrArtifact(":activity:activity-ktx"))
+    api(projectOrArtifact(":lifecycle:lifecycle-viewmodel"))
     api("androidx.compose.ui:ui:1.0.1")
     // old version of common-java8 conflicts with newer version, because both have
     // DefaultLifecycleEventObserver.
diff --git a/activity/activity-compose/src/main/java/androidx/activity/compose/ComponentActivity.kt b/activity/activity-compose/src/main/java/androidx/activity/compose/ComponentActivity.kt
index 6ac414f..b856a6f 100644
--- a/activity/activity-compose/src/main/java/androidx/activity/compose/ComponentActivity.kt
+++ b/activity/activity-compose/src/main/java/androidx/activity/compose/ComponentActivity.kt
@@ -21,9 +21,10 @@
 import androidx.compose.runtime.Composable
 import androidx.compose.runtime.CompositionContext
 import androidx.compose.ui.platform.ComposeView
-import androidx.lifecycle.ViewTreeViewModelStoreOwner
 import androidx.lifecycle.findViewTreeLifecycleOwner
+import androidx.lifecycle.findViewTreeViewModelStoreOwner
 import androidx.lifecycle.setViewTreeLifecycleOwner
+import androidx.lifecycle.setViewTreeViewModelStoreOwner
 import androidx.savedstate.findViewTreeSavedStateRegistryOwner
 import androidx.savedstate.setViewTreeSavedStateRegistryOwner
 
@@ -84,8 +85,8 @@
     if (decorView.findViewTreeLifecycleOwner() == null) {
         decorView.setViewTreeLifecycleOwner(this)
     }
-    if (ViewTreeViewModelStoreOwner.get(decorView) == null) {
-        ViewTreeViewModelStoreOwner.set(decorView, this)
+    if (decorView.findViewTreeViewModelStoreOwner() == null) {
+        decorView.setViewTreeViewModelStoreOwner(this)
     }
     if (decorView.findViewTreeSavedStateRegistryOwner() == null) {
         decorView.setViewTreeSavedStateRegistryOwner(this)
diff --git a/activity/activity/build.gradle b/activity/activity/build.gradle
index 8a8e97b..821f0d8 100644
--- a/activity/activity/build.gradle
+++ b/activity/activity/build.gradle
@@ -30,6 +30,7 @@
     api(libs.kotlinStdlib)
 
     androidTestImplementation(projectOrArtifact(":lifecycle:lifecycle-runtime-testing"))
+    androidTestImplementation(projectOrArtifact(":lifecycle:lifecycle-viewmodel"))
     androidTestImplementation(libs.kotlinStdlib)
     androidTestImplementation(libs.espressoCore, excludes.espresso)
     androidTestImplementation(libs.leakcanary)
diff --git a/activity/activity/src/androidTest/java/androidx/activity/ContentViewTest.kt b/activity/activity/src/androidTest/java/androidx/activity/ContentViewTest.kt
index 7b9523f..fe85d84 100644
--- a/activity/activity/src/androidTest/java/androidx/activity/ContentViewTest.kt
+++ b/activity/activity/src/androidTest/java/androidx/activity/ContentViewTest.kt
@@ -21,8 +21,8 @@
 import android.view.ViewGroup.LayoutParams.WRAP_CONTENT
 import android.widget.TextView
 import androidx.activity.test.R
-import androidx.lifecycle.ViewTreeViewModelStoreOwner
 import androidx.lifecycle.findViewTreeLifecycleOwner
+import androidx.lifecycle.findViewTreeViewModelStoreOwner
 import androidx.savedstate.findViewTreeSavedStateRegistryOwner
 import androidx.test.core.app.ActivityScenario
 import androidx.test.ext.junit.runners.AndroidJUnit4
@@ -62,7 +62,7 @@
                     .that(inflatedTextView.findViewTreeLifecycleOwner())
                     .isSameInstanceAs(this@withActivity)
                 assertWithMessage("inflated view has correct ViewTreeViewModelStoreOwner")
-                    .that(ViewTreeViewModelStoreOwner.get(inflatedTextView))
+                    .that(inflatedTextView.findViewTreeViewModelStoreOwner())
                     .isSameInstanceAs(this@withActivity)
                 assertWithMessage("inflated view has correct ViewTreeSavedStateRegistryOwner")
                     .that(inflatedTextView.findViewTreeSavedStateRegistryOwner())
@@ -100,7 +100,7 @@
 
                     override fun onViewAttachedToWindow(v: View) {
                         attachedLifecycleOwner = view.findViewTreeLifecycleOwner()
-                        attachedViewModelStoreOwner = ViewTreeViewModelStoreOwner.get(view)
+                        attachedViewModelStoreOwner = view.findViewTreeViewModelStoreOwner()
                         attachedSavedStateRegistryOwner = view.findViewTreeSavedStateRegistryOwner()
                     }
                 })
diff --git a/annotation/annotation-experimental-lint/src/test/kotlin/androidx/annotation/experimental/lint/ExperimentalDetectorTest.kt b/annotation/annotation-experimental-lint/src/test/kotlin/androidx/annotation/experimental/lint/ExperimentalDetectorTest.kt
index 167577e..62fa26e 100644
--- a/annotation/annotation-experimental-lint/src/test/kotlin/androidx/annotation/experimental/lint/ExperimentalDetectorTest.kt
+++ b/annotation/annotation-experimental-lint/src/test/kotlin/androidx/annotation/experimental/lint/ExperimentalDetectorTest.kt
@@ -517,7 +517,18 @@
          */
         val SAMPLE_FOO_PACKAGE_INFO: TestFile = base64gzip(
             "libs/sample.experimental.foo.package-info.jar",
-            "UEsDBBQACAgIAGhi/VAAAAAAAAAAAAAAAAAJAAQATUVUQS1JTkYv/soAAAMAUEsHCAAAAAACAAAAAAAAAFBLAwQUAAgICABoYv1QAAAAAAAAAAAAAAAAFAAAAE1FVEEtSU5GL01BTklGRVNULk1G803My0xLLS7RDUstKs7Mz7NSMNQz4OVyLkpNLElN0XWqBAoARfRMFDT8ixKTc1IVnPOLCvKLEkuAijV5uXi5AFBLBwiVBramQAAAAEIAAABQSwMECgAACAAAE2L9UAAAAAAAAAAAAAAAAAcAAABzYW1wbGUvUEsDBAoAAAgAABNi/VAAAAAAAAAAAAAAAAAUAAAAc2FtcGxlL2V4cGVyaW1lbnRhbC9QSwMECgAACAAAGWL9UAAAAAAAAAAAAAAAABgAAABzYW1wbGUvZXhwZXJpbWVudGFsL2Zvby9QSwMEFAAICAgAGWL9UAAAAAAAAAAAAAAAACoAAABzYW1wbGUvZXhwZXJpbWVudGFsL2Zvby9wYWNrYWdlLWluZm8uY2xhc3N1Tb0OgkAY6/kD6qSLi6sr3uLm5KCJiYlGn+AgH+Tw+I7AQXw2Bx/AhzKiLCx2aJO2aV/vxxPAGmMfvo+RwORqqyKivTYkMMtVdFMJBZpju0pVrQQWl4qdzujAtS51aGjLbJ1y2nIpEBxLleWGJN1zKpoaO2VkbK3cdYxzO7sRWP6rd58Fpt9vaRQn8hSmFLk5INBDix76Px5g2KjXJB7wAVBLBwjUtjrHoQAAANsAAABQSwECFAAUAAgICABoYv1QAAAAAAIAAAAAAAAACQAEAAAAAAAAAAAAAAAAAAAATUVUQS1JTkYv/soAAFBLAQIUABQACAgIAGhi/VCVBramQAAAAEIAAAAUAAAAAAAAAAAAAAAAAD0AAABNRVRBLUlORi9NQU5JRkVTVC5NRlBLAQIKAAoAAAgAABNi/VAAAAAAAAAAAAAAAAAHAAAAAAAAAAAAAAAAAL8AAABzYW1wbGUvUEsBAgoACgAACAAAE2L9UAAAAAAAAAAAAAAAABQAAAAAAAAAAAAAAAAA5AAAAHNhbXBsZS9leHBlcmltZW50YWwvUEsBAgoACgAACAAAGWL9UAAAAAAAAAAAAAAAABgAAAAAAAAAAAAAAAAAFgEAAHNhbXBsZS9leHBlcmltZW50YWwvZm9vL1BLAQIUABQACAgIABli/VDUtjrHoQAAANsAAAAqAAAAAAAAAAAAAAAAAEwBAABzYW1wbGUvZXhwZXJpbWVudGFsL2Zvby9wYWNrYWdlLWluZm8uY2xhc3NQSwUGAAAAAAYABgCSAQAARQIAAAAA"
+                "H4sIAAAAAAAA/wvwZmYRYeDg4GDISPobwIAEOBlYGHxdQxx1Pf3c9P+dYmBg" +
+                "ZgjwZucASTFBlQTg1CwCxHDNvo5+nm6uwSF6vm6ffc+c9vHW1bvI662rde7M" +
+                "+c1BBleMHzwt0vPy1fH0vVi6ioWLwfWLj4jJn26hycVBonM+d3N96hbybugy" +
+                "rdxZsRPsgqls25Y5AM13grqAi4EB6CphNBewA3FxYm5BTqo+bkUiCEWpFQWp" +
+                "RZm5qXkliTlIOiTRdEjg0JGWn6+PCA50XVp4dBUkJmcnpqfqZual5esl5yQW" +
+                "F5f67uVrcpB4/ZP51ZLu7tXa9x49e7Kgs7PTbf4DBfknH370HXCsMWOXP9Bu" +
+                "tEhHpyxj8rbM+PfHhQ9IJcvv65944EnWaqVFe81UDE6HlgRzss5K3u0VupZF" +
+                "bHrX3HMvDmzVK83IOJ0zt+hWkaaAjPfUp20qd4u1ZklZp6bkrL1T2lNsvVsw" +
+                "4t/q8vmsy+7nZ4qofxJZJrLTUuGCc7fcL3u5hBsrrqvIfWAExcKVbVbHFwK9" +
+                "dRscC4xMIgyoKQGWRkDJCBWgJCp0rciRK4KizRZHkgKZwMWAOxEgwH7kJIFb" +
+                "E6q1T3AmEYQJ2BIJAogx4ksyCO+DTEEOVS0UU3zwmIKZhAK8WdlAutiAcBJQ" +
+                "pys4OgCGehbu7QMAAA=="
         )
     }
     /* ktlint-enable max-line-length */
diff --git a/annotation/annotation-experimental-lint/src/test/kotlin/androidx/annotation/experimental/lint/KotlinAnnotationsDetectorTest.kt b/annotation/annotation-experimental-lint/src/test/kotlin/androidx/annotation/experimental/lint/KotlinAnnotationsDetectorTest.kt
index cfefb3a..e9e64d7 100644
--- a/annotation/annotation-experimental-lint/src/test/kotlin/androidx/annotation/experimental/lint/KotlinAnnotationsDetectorTest.kt
+++ b/annotation/annotation-experimental-lint/src/test/kotlin/androidx/annotation/experimental/lint/KotlinAnnotationsDetectorTest.kt
@@ -407,7 +407,18 @@
          */
         val SAMPLE_FOO_PACKAGE_INFO: TestFile = base64gzip(
             "libs/sample.kotlin.foo.package-info.jar",
-            "UEsDBBQACAgIAJZj/VQAAAAAAAAAAAAAAAAJAAQATUVUQS1JTkYv/soAAAMAUEsHCAAAAAACAAAAAAAAAFBLAwQUAAgICACWY/1UAAAAAAAAAAAAAAAAFAAAAE1FVEEtSU5GL01BTklGRVNULk1G803My0xLLS7RDUstKs7Mz7NSMNQz4OVyLkpNLElN0XWqBAoARfQMjRU03PPz03NSFTzzkvU0ebl4uQBQSwcIk36TrzsAAAA8AAAAUEsDBAoAAAgAAJBj/VQAAAAAAAAAAAAAAAAHAAAAc2FtcGxlL1BLAwQKAAAIAACQY/1UAAAAAAAAAAAAAAAADgAAAHNhbXBsZS9rb3RsaW4vUEsDBAoAAAgAAJNj/VQAAAAAAAAAAAAAAAASAAAAc2FtcGxlL2tvdGxpbi9mb28vUEsDBBQACAgIAJNj/VQAAAAAAAAAAAAAAAAkAAAAc2FtcGxlL2tvdGxpbi9mb28vcGFja2FnZS1pbmZvLmNsYXNzVY09CsJAFIRn40/UShsbwQMIuo2dlYWCIgh6gpewCZts3oZkEzybhQfwUGLUQp1ipphvmPvjegOwRN+H76MnMDjbqgjVVhslMMopTClWc82RXSRUk8DkVLHTmdpxrUsdGLVmto6ctlwKzA4lZblRMrXOaJabS66KhmZHZt/sv/BKYPrPRtbK30OB4etSGuJYHoNEhW4MCHj4yEPr7W10muw2TRd4AlBLBwhDTi5bpgAAANIAAABQSwECFAAUAAgICACWY/1UAAAAAAIAAAAAAAAACQAEAAAAAAAAAAAAAAAAAAAATUVUQS1JTkYv/soAAFBLAQIUABQACAgIAJZj/VSTfpOvOwAAADwAAAAUAAAAAAAAAAAAAAAAAD0AAABNRVRBLUlORi9NQU5JRkVTVC5NRlBLAQIKAAoAAAgAAJBj/VQAAAAAAAAAAAAAAAAHAAAAAAAAAAAAAAAAALoAAABzYW1wbGUvUEsBAgoACgAACAAAkGP9VAAAAAAAAAAAAAAAAA4AAAAAAAAAAAAAAAAA3wAAAHNhbXBsZS9rb3RsaW4vUEsBAgoACgAACAAAk2P9VAAAAAAAAAAAAAAAABIAAAAAAAAAAAAAAAAACwEAAHNhbXBsZS9rb3RsaW4vZm9vL1BLAQIUABQACAgIAJNj/VRDTi5bpgAAANIAAAAkAAAAAAAAAAAAAAAAADsBAABzYW1wbGUva290bGluL2Zvby9wYWNrYWdlLWluZm8uY2xhc3NQSwUGAAAAAAYABgCAAQAAMwIAAAAA"
+            "H4sIAAAAAAAA/wvwZmYRYeDg4GCYlvw3hAEJcDKwMPi6hjjqevq56f87xcDA" +
+                "zBDgzc4BkmKCKgnAqVkEiOGafR39PN1cg0P0fN0++5457eOtq3eR11tX69yZ" +
+                "85uDDK4YP3hapOflq+Ppe7F0FQsXg+sXnl5RkzufP18uDhK1+Tzpq0nlzoqd" +
+                "YMsn101ebw002gZqORcDA9BBE9AsZwfi4sTcgpxUfdyK+BCKsvNLcjLzkNRO" +
+                "RlMrhKE2LT9fH+F7dPUqWNUXJCZnJ6an6mbmpeXrJeckFheH9tpyHXIQaUl/" +
+                "7H/FS1r6IDPHrt65U1ublDiqmqZv4Jydc68tRdhmdiv7h4CkK05zk5bNyJ/x" +
+                "+3EV8waX++3vF6sbWNxexXE1TFrV4JSmj2ZY8dmJsSohkw88Cdl4eeatwrXe" +
+                "shJb07b1zdkWw3WGTzV1Z6DR1nMZ02Z7r+tqS3NPu/9m/wevhF/n3a6duu/c" +
+                "+PB1kNSjCLlml9Y8Ho6KHyecX7/NLZn1xsxXvIIJFPDOfnrRy4C+ugQOeEYm" +
+                "EQbUeIelCFCiQQUoSQhdK3J8iqBos8WRgEAmcDHgjncE2IWcCnBr4kPRdB9L" +
+                "qkDoxZYuEICbEXsqQXgZpB85JFVQ9Ftj1Y+ZagK8WdlA6tmAsAGoxxgc+ACq" +
+                "I6JIyQMAAA=="
         )
     }
     /* ktlint-enable max-line-length */
diff --git a/annotation/annotation-experimental-lint/src/test/kotlin/androidx/annotation/experimental/lint/RequiresOptInDetectorTest.kt b/annotation/annotation-experimental-lint/src/test/kotlin/androidx/annotation/experimental/lint/RequiresOptInDetectorTest.kt
index 5a2bcf3..89a38ed 100644
--- a/annotation/annotation-experimental-lint/src/test/kotlin/androidx/annotation/experimental/lint/RequiresOptInDetectorTest.kt
+++ b/annotation/annotation-experimental-lint/src/test/kotlin/androidx/annotation/experimental/lint/RequiresOptInDetectorTest.kt
@@ -522,7 +522,18 @@
          */
         val SAMPLE_FOO_PACKAGE_INFO: TestFile = base64gzip(
             "libs/sample.optin.foo.package-info.jar",
-            "UEsDBBQACAgIABRYjVIAAAAAAAAAAAAAAAAJAAQATUVUQS1JTkYv/soAAAMAUEsHCAAAAAACAAAAAAAAAFBLAwQUAAgICAAUWI1SAAAAAAAAAAAAAAAAFAAAAE1FVEEtSU5GL01BTklGRVNULk1G803My0xLLS7RDUstKs7Mz7NSMNQz4OVyLkpNLElN0XWqBAoARfRMFDT8ixKTc1IVnPOLCvKLEkuAijV5uXi5AFBLBwiVBramQAAAAEIAAABQSwMECgAACAAAOVeNUgAAAAAAAAAAAAAAAAcAAABzYW1wbGUvUEsDBAoAAAgAADlXjVIAAAAAAAAAAAAAAAANAAAAc2FtcGxlL29wdGluL1BLAwQKAAAIAAA7V41SAAAAAAAAAAAAAAAAEQAAAHNhbXBsZS9vcHRpbi9mb28vUEsDBBQACAgIADtXjVIAAAAAAAAAAAAAAAAjAAAAc2FtcGxlL29wdGluL2Zvby9wYWNrYWdlLWluZm8uY2xhc3NVjcEOwUAYhGeLFicuLuIBHNiLm5MDCZFIeIJts222tv9u2m3j2Rw8gIcSiwPmMHOYbzL3x/UGYIFehChCl6F/MnWZyI3SkmFoRXIWmZwpSs08F41gGB9rcqqQW2pUpWItV0TGCacMVQzTfSUKqyU31ini64uVpYfJCb3z8y+7ZJj8oakx/PeOYfA65FpQxg9xLhM3AhgCfBSg9fY2Oj5D34TAE1BLBwjeUT3SpAAAANAAAABQSwECFAAUAAgICAAUWI1SAAAAAAIAAAAAAAAACQAEAAAAAAAAAAAAAAAAAAAATUVUQS1JTkYv/soAAFBLAQIUABQACAgIABRYjVKVBramQAAAAEIAAAAUAAAAAAAAAAAAAAAAAD0AAABNRVRBLUlORi9NQU5JRkVTVC5NRlBLAQIKAAoAAAgAADlXjVIAAAAAAAAAAAAAAAAHAAAAAAAAAAAAAAAAAL8AAABzYW1wbGUvUEsBAgoACgAACAAAOVeNUgAAAAAAAAAAAAAAAA0AAAAAAAAAAAAAAAAA5AAAAHNhbXBsZS9vcHRpbi9QSwECCgAKAAAIAAA7V41SAAAAAAAAAAAAAAAAEQAAAAAAAAAAAAAAAAAPAQAAc2FtcGxlL29wdGluL2Zvby9QSwECFAAUAAgICAA7V41S3lE90qQAAADQAAAAIwAAAAAAAAAAAAAAAAA+AQAAc2FtcGxlL29wdGluL2Zvby9wYWNrYWdlLWluZm8uY2xhc3NQSwUGAAAAAAYABgB9AQAAMwIAAAAA"
+            "H4sIAAAAAAAA/wvwZmYRYeDg4GAQiegNYkACnAwsDL6uIY66nn5u+v9OMTAw" +
+                "MwR4s3OApJigSgJwahYBYrhmX0c/TzfX4BA9X7fPvmdO+3jr6l3k9dbVOnfm" +
+                "/OYggyvGD54W6Xn56nj6XixdxcLF4PrFR8TkT7fQ5OIg0Tmfu7k+dQt5N3SZ" +
+                "Vu6s2Al2wVS2bcscgOY7QV3AxcAAdJVlOKoL2IG4ODG3ICdVH7ciXoSi/IKS" +
+                "zDwkpdZoSgXRlabl5+sjAgBduTI25QWJydmJ6am6mXlp+XrJOYnFxaG9B/kO" +
+                "Oki0pHeLqevpPWKUudE9ezIz50SPiqbczbnbtv3Pu5X7+KaMTUO7UDfzM4Pi" +
+                "GflG349/ZUtojGvRcJq+sN6odOaJ3kuTEjNci8RmztH0Omsj3psgIZ9dtGpC" +
+                "dFbI0iTdcJdjnMt5Qnku16pyrVY1v6b56HX31KXtJzn3fv6svztlxp+FKw3/" +
+                "fO9L/GD1JCrgGH+hnrA5kwRTjciCr9/MrOyc77ccEAaF+b1A20tLgF66AA5z" +
+                "RiYRBtR4h6UIUKJBBShJCF0rclSKoGizxZGAQCZwMeCOcgTYj5wAcGviRdH0" +
+                "BDNBILRiSxIIwM+INYEgPAzSjhyOyija7bBpx0wwAd6sbCDlbEBYC9RiDA55" +
+                "AGF9KXfGAwAA"
         )
 
         /**
@@ -532,7 +543,18 @@
          */
         val SAMPLE_BAR_PACKAGE_INFO: TestFile = base64gzip(
             "libs/sample.optin.bar.package-info.jar",
-            "UEsDBBQACAgIAEZucVQAAAAAAAAAAAAAAAAJAAQATUVUQS1JTkYv/soAAAMAUEsHCAAAAAACAAAAAAAAAFBLAwQUAAgICABGbnFUAAAAAAAAAAAAAAAAFAAAAE1FVEEtSU5GL01BTklGRVNULk1G803My0xLLS7RDUstKs7Mz7NSMNQz4OVyLkpNLElN0XWqBAoARfQMjRQ03PPz03NSFTzzkvU0ebl4uQBQSwcIy/5xeDsAAAA8AAAAUEsDBAoAAAgAAC5bcVQAAAAAAAAAAAAAAAAHAAAAc2FtcGxlL1BLAwQKAAAIAAAuW3FUAAAAAAAAAAAAAAAADQAAAHNhbXBsZS9vcHRpbi9QSwMECgAACAAALltxVAAAAAAAAAAAAAAAABEAAABzYW1wbGUvb3B0aW4vYmFyL1BLAwQUAAgICAA1bnFUAAAAAAAAAAAAAAAAIwAAAHNhbXBsZS9vcHRpbi9iYXIvcGFja2FnZS1pbmZvLmNsYXNzVU5LagJBEH1tPhMVgtm4CTlAFrE32WUVxIBBEHSZVc1MZeixp7rpacWzZZED5FBiqxCTgqqCep96P7uvbwDP6GfoZugp9JZuHQp+M5YV7jwVK6r4ycinG9W0IYX7xVqiaXgqG9Oa3PKriIsUjZM2oTOSMjhTbjX93vXcx6m8KPQbCisOY0tt4j7OWmq8Ze18NKInW88hGUsk+55enX2T8uEfNaeg/0ZTGBzCaUtS6XlecxGHgEIHp+rg4jgvcZX2bUKuU2cfUAVu9lBLBwjs0mNTygAAAAQBAABQSwECFAAUAAgICABGbnFUAAAAAAIAAAAAAAAACQAEAAAAAAAAAAAAAAAAAAAATUVUQS1JTkYv/soAAFBLAQIUABQACAgIAEZucVTL/nF4OwAAADwAAAAUAAAAAAAAAAAAAAAAAD0AAABNRVRBLUlORi9NQU5JRkVTVC5NRlBLAQIKAAoAAAgAAC5bcVQAAAAAAAAAAAAAAAAHAAAAAAAAAAAAAAAAALoAAABzYW1wbGUvUEsBAgoACgAACAAALltxVAAAAAAAAAAAAAAAAA0AAAAAAAAAAAAAAAAA3wAAAHNhbXBsZS9vcHRpbi9QSwECCgAKAAAIAAAuW3FUAAAAAAAAAAAAAAAAEQAAAAAAAAAAAAAAAAAKAQAAc2FtcGxlL29wdGluL2Jhci9QSwECFAAUAAgICAA1bnFU7NJjU8oAAAAEAQAAIwAAAAAAAAAAAAAAAAA5AQAAc2FtcGxlL29wdGluL2Jhci9wYWNrYWdlLWluZm8uY2xhc3NQSwUGAAAAAAYABgB9AQAAVAIAAAAA"
+            "H4sIAAAAAAAA/wvwZmYRYeDg4GBwyysMYUACnAwsDL6uIY66nn5u+v9OMTAw" +
+                "MwR4s3OApJigSgJwahYBYrhmX0c/TzfX4BA9X7fPvmdO+3jr6l3k9dbVOnfm" +
+                "/OYggyvGD54W6Xn56nj6XixdxcLF4PqFp1fE5M7nz5eLg0RtPk/6alK5s2In" +
+                "2PLT/worrIFG20At52JgADpILxrVcnYgLk7MLchJ1cetiBehKL+gJDMPj1JB" +
+                "dKVJiUX6CL+bovldGZvygsTk7MT0VN3MvLR8veScxOLiUD/vLCZHgdpcO2HR" +
+                "pps7OC0dxDaa30wVPdLgKFAyM/SsT+qLjct3vcw8ujl1IvOTgKTVApObVjVV" +
+                "za+y370+n+H8i/QXaS80v0zLk+WqM54m+s5GVHvVj5Mnlktf3aLY+vto1KLM" +
+                "Ci3py7PufFrd0S3SO9lsofEkI4vgPNO/977eOb5yj8YXaS5tvmTv3Ed256Ky" +
+                "9qS+rTFZpB59XlHSW+X3vLi5tZM/PZQ3Xb7gv1uwhMyhTO+gl5VxxYLtDU7s" +
+                "y189eGShXzj1W67TuuB0+QDWvG+gmHlzKTkYmEYYWBhBMcPIJMKAmjBgSQaU" +
+                "qlABShpD14oc4SIo2mxxpDCQCVwMuBMGAuxCTia4NfGiaLqPmWxwaxVE0crF" +
+                "iDUZITwMSkjI4aiMot0Sm3bMZBXgzcoGUs4GhLVALSHgkAcAwvFVfOcDAAA="
         )
     }
     /* ktlint-enable max-line-length */
diff --git a/appactions/interaction/interaction-capabilities-core/api/current.txt b/appactions/interaction/interaction-capabilities-core/api/current.txt
new file mode 100644
index 0000000..e6f50d0
--- /dev/null
+++ b/appactions/interaction/interaction-capabilities-core/api/current.txt
@@ -0,0 +1 @@
+// Signature format: 4.0
diff --git a/appactions/interaction/interaction-capabilities-core/api/public_plus_experimental_current.txt b/appactions/interaction/interaction-capabilities-core/api/public_plus_experimental_current.txt
new file mode 100644
index 0000000..e6f50d0
--- /dev/null
+++ b/appactions/interaction/interaction-capabilities-core/api/public_plus_experimental_current.txt
@@ -0,0 +1 @@
+// Signature format: 4.0
diff --git a/appactions/interaction/interaction-capabilities-core/api/res-current.txt b/appactions/interaction/interaction-capabilities-core/api/res-current.txt
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/appactions/interaction/interaction-capabilities-core/api/res-current.txt
diff --git a/appactions/interaction/interaction-capabilities-core/api/restricted_current.txt b/appactions/interaction/interaction-capabilities-core/api/restricted_current.txt
new file mode 100644
index 0000000..e6f50d0
--- /dev/null
+++ b/appactions/interaction/interaction-capabilities-core/api/restricted_current.txt
@@ -0,0 +1 @@
+// Signature format: 4.0
diff --git a/appactions/interaction/interaction-capabilities-core/build.gradle b/appactions/interaction/interaction-capabilities-core/build.gradle
new file mode 100644
index 0000000..066897d
--- /dev/null
+++ b/appactions/interaction/interaction-capabilities-core/build.gradle
@@ -0,0 +1,36 @@
+/*
+ * Copyright (C) 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+import androidx.build.LibraryType
+
+plugins {
+    id("AndroidXPlugin")
+    id("com.android.library")
+}
+
+dependencies {
+}
+
+android {
+    namespace "androidx.appactions.interaction.capabilities.core"
+}
+
+androidx {
+    name = "androidx.appactions.interaction:interaction-capabilities-core"
+    type = LibraryType.PUBLISHED_LIBRARY
+    inceptionYear = "2023"
+    description = "App Interaction library core capabilities API and implementation."
+}
diff --git a/appcompat/appcompat/build.gradle b/appcompat/appcompat/build.gradle
index ae36959..a2d862c 100644
--- a/appcompat/appcompat/build.gradle
+++ b/appcompat/appcompat/build.gradle
@@ -48,6 +48,7 @@
     androidTestImplementation(libs.espressoCore, excludes.espresso)
     androidTestImplementation(libs.mockitoCore, excludes.bytebuddy) // DexMaker has it's own MockMaker
     androidTestImplementation(libs.dexmakerMockito, excludes.bytebuddy) // DexMaker has it's own MockMaker
+    androidTestImplementation(projectOrArtifact(":lifecycle:lifecycle-viewmodel"))
     androidTestImplementation("androidx.lifecycle:lifecycle-runtime-testing:2.5.1", {
 	    // Needed to ensure that the same version of lifecycle-runtime-ktx
 	    // is pulled into main and androidTest configurations. Otherwise,
diff --git a/appcompat/appcompat/src/androidTest/java/androidx/appcompat/app/AppCompatActivityViewTreeTest.kt b/appcompat/appcompat/src/androidTest/java/androidx/appcompat/app/AppCompatActivityViewTreeTest.kt
index dba4da4..e3dafc5 100644
--- a/appcompat/appcompat/src/androidTest/java/androidx/appcompat/app/AppCompatActivityViewTreeTest.kt
+++ b/appcompat/appcompat/src/androidTest/java/androidx/appcompat/app/AppCompatActivityViewTreeTest.kt
@@ -17,9 +17,9 @@
 package androidx.appcompat.app
 
 import androidx.lifecycle.findViewTreeLifecycleOwner
-import androidx.lifecycle.ViewTreeViewModelStoreOwner
 import androidx.savedstate.findViewTreeSavedStateRegistryOwner
 import androidx.activity.findViewTreeOnBackPressedDispatcherOwner
+import androidx.lifecycle.findViewTreeViewModelStoreOwner
 import androidx.test.ext.junit.runners.AndroidJUnit4
 import androidx.test.filters.MediumTest
 import com.google.common.truth.Truth.assertThat
@@ -44,7 +44,7 @@
 
     @Test
     fun queryViewTreeViewModelStoreTest() {
-        val vmsOwner = ViewTreeViewModelStoreOwner.get(activityRule.activity.window.decorView)
+        val vmsOwner = activityRule.activity.window.decorView.findViewTreeViewModelStoreOwner()
         assertThat(vmsOwner).isEqualTo(activityRule.activity)
     }
 
diff --git a/appsearch/appsearch-local-storage/src/androidTest/java/androidx/appsearch/localstorage/AppSearchImplTest.java b/appsearch/appsearch-local-storage/src/androidTest/java/androidx/appsearch/localstorage/AppSearchImplTest.java
index d972613..4fef3f4 100644
--- a/appsearch/appsearch-local-storage/src/androidTest/java/androidx/appsearch/localstorage/AppSearchImplTest.java
+++ b/appsearch/appsearch-local-storage/src/androidTest/java/androidx/appsearch/localstorage/AppSearchImplTest.java
@@ -32,6 +32,7 @@
 import androidx.appsearch.app.GenericDocument;
 import androidx.appsearch.app.GetSchemaResponse;
 import androidx.appsearch.app.InternalSetSchemaResponse;
+import androidx.appsearch.app.JoinSpec;
 import androidx.appsearch.app.PackageIdentifier;
 import androidx.appsearch.app.SearchResult;
 import androidx.appsearch.app.SearchResultPage;
@@ -3323,6 +3324,18 @@
     }
 
     @Test
+    public void testRemoveByQuery_withJoinSpec_throwsException() {
+        Exception e = assertThrows(IllegalArgumentException.class,
+                () -> mAppSearchImpl.removeByQuery("", "", "",
+                        new SearchSpec.Builder()
+                                .setJoinSpec(new JoinSpec.Builder("childProp").build())
+                                .build(),
+                        null));
+        assertThat(e.getMessage()).isEqualTo(
+                "JoinSpec not allowed in removeByQuery, but JoinSpec was provided");
+    }
+
+    @Test
     public void testLimitConfig_Replace() throws Exception {
         // Create a new mAppSearchImpl with a lower limit
         mAppSearchImpl.close();
diff --git a/appsearch/appsearch-local-storage/src/main/java/androidx/appsearch/localstorage/AppSearchImpl.java b/appsearch/appsearch-local-storage/src/main/java/androidx/appsearch/localstorage/AppSearchImpl.java
index 2307cde..f445af7 100644
--- a/appsearch/appsearch-local-storage/src/main/java/androidx/appsearch/localstorage/AppSearchImpl.java
+++ b/appsearch/appsearch-local-storage/src/main/java/androidx/appsearch/localstorage/AppSearchImpl.java
@@ -43,6 +43,7 @@
 import androidx.appsearch.app.GetByDocumentIdRequest;
 import androidx.appsearch.app.GetSchemaResponse;
 import androidx.appsearch.app.InternalSetSchemaResponse;
+import androidx.appsearch.app.JoinSpec;
 import androidx.appsearch.app.PackageIdentifier;
 import androidx.appsearch.app.SearchResultPage;
 import androidx.appsearch.app.SearchSpec;
@@ -1825,18 +1826,26 @@
      *
      * <p>This method belongs to mutate group.
      *
+     * <p> {@link SearchSpec} objects containing a {@link JoinSpec} are not allowed here.
+     *
      * @param packageName        The package name that owns the documents.
      * @param databaseName       The databaseName the document is in.
      * @param queryExpression    Query String to search.
      * @param searchSpec         Defines what and how to remove
      * @param removeStatsBuilder builder for {@link RemoveStats} to hold stats for remove
      * @throws AppSearchException on IcingSearchEngine error.
+     * @throws IllegalArgumentException if the {@link SearchSpec} contains a {@link JoinSpec}.
      */
     public void removeByQuery(@NonNull String packageName, @NonNull String databaseName,
             @NonNull String queryExpression,
             @NonNull SearchSpec searchSpec,
             @Nullable RemoveStats.Builder removeStatsBuilder)
             throws AppSearchException {
+        if (searchSpec.getJoinSpec() != null) {
+            throw new IllegalArgumentException("JoinSpec not allowed in removeByQuery, but "
+                    + "JoinSpec was provided");
+        }
+
         long totalLatencyStartTimeMillis = SystemClock.elapsedRealtime();
         mReadWriteLock.writeLock().lock();
         try {
diff --git a/appsearch/appsearch-local-storage/src/main/java/androidx/appsearch/localstorage/SearchSessionImpl.java b/appsearch/appsearch-local-storage/src/main/java/androidx/appsearch/localstorage/SearchSessionImpl.java
index f66b650..36644b4 100644
--- a/appsearch/appsearch-local-storage/src/main/java/androidx/appsearch/localstorage/SearchSessionImpl.java
+++ b/appsearch/appsearch-local-storage/src/main/java/androidx/appsearch/localstorage/SearchSessionImpl.java
@@ -508,6 +508,12 @@
             @NonNull String queryExpression, @NonNull SearchSpec searchSpec) {
         Preconditions.checkNotNull(queryExpression);
         Preconditions.checkNotNull(searchSpec);
+
+        if (searchSpec.getJoinSpec() != null) {
+            throw new IllegalArgumentException("JoinSpec not allowed in removeByQuery, but "
+                    + "JoinSpec was provided.");
+        }
+
         Preconditions.checkState(!mIsClosed, "AppSearchSession has already been closed");
         ListenableFuture<Void> future = execute(() -> {
             RemoveStats.Builder removeStatsBuilder = null;
diff --git a/appsearch/appsearch-platform-storage/src/main/java/androidx/appsearch/platformstorage/SearchSessionImpl.java b/appsearch/appsearch-platform-storage/src/main/java/androidx/appsearch/platformstorage/SearchSessionImpl.java
index 76e6450..a4621c2 100644
--- a/appsearch/appsearch-platform-storage/src/main/java/androidx/appsearch/platformstorage/SearchSessionImpl.java
+++ b/appsearch/appsearch-platform-storage/src/main/java/androidx/appsearch/platformstorage/SearchSessionImpl.java
@@ -204,6 +204,11 @@
         Preconditions.checkNotNull(searchSpec);
         ResolvableFuture<Void> future = ResolvableFuture.create();
 
+        if (searchSpec.getJoinSpec() != null) {
+            throw new IllegalArgumentException("JoinSpec not allowed in removeByQuery, but "
+                    + "JoinSpec was provided.");
+        }
+
         if (!BuildCompat.isAtLeastT() && !searchSpec.getFilterNamespaces().isEmpty()) {
             // This is a patch for b/197361770, framework-appsearch in Android S will
             // disable the given namespace filter if it is not empty and none of given namespaces
diff --git a/appsearch/appsearch/src/androidTest/java/androidx/appsearch/cts/app/AppSearchSessionCtsTestBase.java b/appsearch/appsearch/src/androidTest/java/androidx/appsearch/cts/app/AppSearchSessionCtsTestBase.java
index bac049f..fb31d2e 100644
--- a/appsearch/appsearch/src/androidTest/java/androidx/appsearch/cts/app/AppSearchSessionCtsTestBase.java
+++ b/appsearch/appsearch/src/androidTest/java/androidx/appsearch/cts/app/AppSearchSessionCtsTestBase.java
@@ -3220,6 +3220,19 @@
     }
 
     @Test
+    public void testRemoveQueryWithJoinSpecThrowsException() {
+        assumeTrue(mDb1.getFeatures().isFeatureSupported(Features.JOIN_SPEC_AND_QUALIFIED_ID));
+
+        IllegalArgumentException e = assertThrows(IllegalArgumentException.class,
+                () -> mDb2.removeAsync("",
+                new SearchSpec.Builder()
+                        .setJoinSpec(new JoinSpec.Builder("entityId").build())
+                        .build()));
+        assertThat(e.getMessage()).isEqualTo("JoinSpec not allowed in removeByQuery, "
+                + "but JoinSpec was provided.");
+    }
+
+    @Test
     public void testCloseAndReopen() throws Exception {
         // Schema registration
         mDb1.setSchemaAsync(
@@ -3483,20 +3496,20 @@
     public void testIndexNestedDocuments() throws Exception {
         // Schema registration
         mDb1.setSchemaAsync(new SetSchemaRequest.Builder()
-                .addSchemas(AppSearchEmail.SCHEMA)
-                .addSchemas(new AppSearchSchema.Builder("YesNestedIndex")
-                        .addProperty(new AppSearchSchema.DocumentPropertyConfig.Builder(
-                                "prop", AppSearchEmail.SCHEMA_TYPE)
-                                .setShouldIndexNestedProperties(true)
+                        .addSchemas(AppSearchEmail.SCHEMA)
+                        .addSchemas(new AppSearchSchema.Builder("YesNestedIndex")
+                                .addProperty(new AppSearchSchema.DocumentPropertyConfig.Builder(
+                                        "prop", AppSearchEmail.SCHEMA_TYPE)
+                                        .setShouldIndexNestedProperties(true)
+                                        .build())
+                                .build())
+                        .addSchemas(new AppSearchSchema.Builder("NoNestedIndex")
+                                .addProperty(new AppSearchSchema.DocumentPropertyConfig.Builder(
+                                        "prop", AppSearchEmail.SCHEMA_TYPE)
+                                        .setShouldIndexNestedProperties(false)
+                                        .build())
                                 .build())
                         .build())
-                .addSchemas(new AppSearchSchema.Builder("NoNestedIndex")
-                        .addProperty(new AppSearchSchema.DocumentPropertyConfig.Builder(
-                                "prop", AppSearchEmail.SCHEMA_TYPE)
-                                .setShouldIndexNestedProperties(false)
-                                .build())
-                        .build())
-                .build())
                 .get();
 
         // Index the documents.
diff --git a/appsearch/appsearch/src/androidTest/java/androidx/appsearch/cts/app/GetByDocumentIdRequestCtsTest.java b/appsearch/appsearch/src/androidTest/java/androidx/appsearch/cts/app/GetByDocumentIdRequestCtsTest.java
index 8b2dd34..9f6e995 100644
--- a/appsearch/appsearch/src/androidTest/java/androidx/appsearch/cts/app/GetByDocumentIdRequestCtsTest.java
+++ b/appsearch/appsearch/src/androidTest/java/androidx/appsearch/cts/app/GetByDocumentIdRequestCtsTest.java
@@ -42,7 +42,7 @@
                         .addIds("uri1", "uri2")
                         .addIds(Arrays.asList("uri3", "uri4"))
                         .addProjection("schemaType1", expectedPropertyPaths1)
-                        .addProjection("schemaType2", expectedPropertyPaths2)
+                        .addProjectionPaths("schemaType2", expectedPropertyPathObjects2)
                         .build();
 
         assertThat(getByDocumentIdRequest.getIds()).containsExactly(
diff --git a/appsearch/appsearch/src/main/java/androidx/appsearch/app/AppSearchSession.java b/appsearch/appsearch/src/main/java/androidx/appsearch/app/AppSearchSession.java
index 06a0d6b..16098ebc 100644
--- a/appsearch/appsearch/src/main/java/androidx/appsearch/app/AppSearchSession.java
+++ b/appsearch/appsearch/src/main/java/androidx/appsearch/app/AppSearchSession.java
@@ -312,6 +312,9 @@
      *                        indicates how document will be removed. All specific about how to
      *                        scoring, ordering, snippeting and resulting will be ignored.
      * @return The pending result of performing this operation.
+     * @throws IllegalArgumentException if the {@link SearchSpec} contains a {@link JoinSpec}.
+     * {@link JoinSpec} lets you join docs that are not owned by the caller, so the semantics of
+     * failures from this method would be complex.
      */
     @NonNull
     ListenableFuture<Void> removeAsync(@NonNull String queryExpression,
diff --git a/appsearch/appsearch/src/main/java/androidx/appsearch/app/SearchSpec.java b/appsearch/appsearch/src/main/java/androidx/appsearch/app/SearchSpec.java
index 1f92e9a..b469087 100644
--- a/appsearch/appsearch/src/main/java/androidx/appsearch/app/SearchSpec.java
+++ b/appsearch/appsearch/src/main/java/androidx/appsearch/app/SearchSpec.java
@@ -1039,9 +1039,11 @@
          *                            weight to set for that property.
          * @throws IllegalArgumentException if a weight is equal to or less than 0.0.
          */
+        // @exportToFramework:startStrip()
         @RequiresFeature(
                 enforcement = "androidx.appsearch.app.Features#isFeatureSupported",
                 name = Features.SEARCH_SPEC_PROPERTY_WEIGHTS)
+        // @exportToFramework:endStrip()
         @NonNull
         public SearchSpec.Builder setPropertyWeights(@NonNull String schemaType,
                 @NonNull Map<String, Double> propertyPathWeights) {
@@ -1113,9 +1115,11 @@
          *                            weight to set for that property.
          * @throws IllegalArgumentException if a weight is equal to or less than 0.0.
          */
+        // @exportToFramework:startStrip()
         @RequiresFeature(
                 enforcement = "androidx.appsearch.app.Features#isFeatureSupported",
                 name = Features.SEARCH_SPEC_PROPERTY_WEIGHTS)
+        // @exportToFramework:endStrip()
         @NonNull
         public SearchSpec.Builder setPropertyWeightPaths(@NonNull String schemaType,
                 @NonNull Map<PropertyPath, Double> propertyPathWeights) {
diff --git a/arch/OWNERS b/arch/OWNERS
index fc51372..70bfefa 100644
--- a/arch/OWNERS
+++ b/arch/OWNERS
@@ -1,2 +1,3 @@
 sergeyv@google.com
-yboyar@google.com
\ No newline at end of file
+yboyar@google.com
+sanura@google.com
\ No newline at end of file
diff --git a/benchmark/benchmark-macro/src/androidTest/java/androidx/benchmark/macro/perfetto/PerfettoSdkTraceTest.kt b/benchmark/benchmark-macro/src/androidTest/java/androidx/benchmark/macro/perfetto/PerfettoSdkTraceTest.kt
index f0b3030..4d0e75b 100644
--- a/benchmark/benchmark-macro/src/androidTest/java/androidx/benchmark/macro/perfetto/PerfettoSdkTraceTest.kt
+++ b/benchmark/benchmark-macro/src/androidTest/java/androidx/benchmark/macro/perfetto/PerfettoSdkTraceTest.kt
@@ -20,12 +20,12 @@
 import androidx.benchmark.junit4.PerfettoTraceRule
 import androidx.benchmark.perfetto.ExperimentalPerfettoCaptureApi
 import androidx.benchmark.perfetto.PerfettoHelper
-import androidx.test.filters.FlakyTest
 import androidx.test.filters.LargeTest
 import androidx.test.filters.SdkSuppress
 import androidx.tracing.trace
 import com.google.common.truth.Truth.assertThat
 import org.junit.Assume.assumeTrue
+import org.junit.Ignore
 import org.junit.Rule
 import org.junit.Test
 import org.junit.runner.RunWith
@@ -62,7 +62,7 @@
         assertThat(actualSlices).containsExactlyElementsIn(expectedSlices)
     }
 
-    @FlakyTest(bugId = 260715950)
+    @Ignore // b/260715950
     @Test
     @SdkSuppress(maxSdkVersion = 33) // b/262909049: Failing on SDK 34
     fun test_endToEnd() {
diff --git a/buildSrc-tests/project-subsets/src/test/kotlin/androidx/build/ProjectSubsetsTest.kt b/buildSrc-tests/project-subsets/src/test/kotlin/androidx/build/ProjectSubsetsTest.kt
index 653ae96..15c073f 100644
--- a/buildSrc-tests/project-subsets/src/test/kotlin/androidx/build/ProjectSubsetsTest.kt
+++ b/buildSrc-tests/project-subsets/src/test/kotlin/androidx/build/ProjectSubsetsTest.kt
@@ -79,6 +79,10 @@
         validateSubset("native")
     }
 
+    @Test
+    fun testSubsetWindow() {
+        validateSubset("window")
+    }
     /**
      * Validates a specific project subset
      */
diff --git a/camera/camera-camera2-pipe-integration/src/androidTest/java/androidx/camera/camera2/pipe/testing/TestUseCaseCamera.kt b/camera/camera-camera2-pipe-integration/src/androidTest/java/androidx/camera/camera2/pipe/testing/TestUseCaseCamera.kt
index a8a75db..9611df2 100644
--- a/camera/camera-camera2-pipe-integration/src/androidTest/java/androidx/camera/camera2/pipe/testing/TestUseCaseCamera.kt
+++ b/camera/camera-camera2-pipe-integration/src/androidTest/java/androidx/camera/camera2/pipe/testing/TestUseCaseCamera.kt
@@ -32,6 +32,7 @@
 import androidx.camera.camera2.pipe.integration.config.CameraConfig
 import androidx.camera.camera2.pipe.integration.config.UseCaseCameraConfig
 import androidx.camera.camera2.pipe.integration.impl.CameraCallbackMap
+import androidx.camera.camera2.pipe.integration.impl.CameraInteropStateCallbackRepository
 import androidx.camera.camera2.pipe.integration.impl.CameraPipeCameraProperties
 import androidx.camera.camera2.pipe.integration.impl.CapturePipeline
 import androidx.camera.camera2.pipe.integration.impl.ComboRequestListener
@@ -71,6 +72,7 @@
             cameraPipe = cameraPipe,
             requestListener = ComboRequestListener(),
             useCaseSurfaceManager = useCaseSurfaceManager,
+            cameraInteropStateCallbackRepository = CameraInteropStateCallbackRepository()
         )
 
     override val requestControl: UseCaseCameraRequestControl = UseCaseCameraRequestControlImpl(
diff --git a/camera/camera-camera2-pipe-integration/src/main/java/androidx/camera/camera2/pipe/integration/config/CameraAppConfig.kt b/camera/camera-camera2-pipe-integration/src/main/java/androidx/camera/camera2/pipe/integration/config/CameraAppConfig.kt
index 9ce1084..2f41d39 100644
--- a/camera/camera-camera2-pipe-integration/src/main/java/androidx/camera/camera2/pipe/integration/config/CameraAppConfig.kt
+++ b/camera/camera-camera2-pipe-integration/src/main/java/androidx/camera/camera2/pipe/integration/config/CameraAppConfig.kt
@@ -22,6 +22,7 @@
 import androidx.annotation.RequiresApi
 import androidx.camera.camera2.pipe.CameraDevices
 import androidx.camera.camera2.pipe.CameraPipe
+import androidx.camera.camera2.pipe.integration.impl.CameraInteropStateCallbackRepository
 import androidx.camera.core.impl.CameraFactory
 import androidx.camera.core.impl.CameraThreadConfig
 import dagger.Component
@@ -37,9 +38,18 @@
     companion object {
         @Singleton
         @Provides
-        fun provideCameraPipe(context: Context): CameraPipe {
-            return CameraPipe(CameraPipe.Config(appContext = context.applicationContext))
-        }
+        fun provideCameraPipe(
+            context: Context,
+            cameraInteropStateCallbackRepository: CameraInteropStateCallbackRepository
+        ): CameraPipe = CameraPipe(
+            CameraPipe.Config(
+                appContext = context.applicationContext,
+                cameraInteropConfig = CameraPipe.CameraInteropConfig(
+                    cameraInteropStateCallbackRepository.deviceStateCallback,
+                    cameraInteropStateCallbackRepository.sessionStateCallback
+                )
+            )
+        )
 
         @Provides
         fun provideCameraDevices(cameraPipe: CameraPipe): CameraDevices {
@@ -52,7 +62,7 @@
 @Module
 class CameraAppConfig(
     private val context: Context,
-    private val cameraThreadConfig: CameraThreadConfig
+    private val cameraThreadConfig: CameraThreadConfig,
 ) {
     @Provides
     fun provideContext(): Context = context
diff --git a/camera/camera-camera2-pipe-integration/src/main/java/androidx/camera/camera2/pipe/integration/config/UseCaseCameraConfig.kt b/camera/camera-camera2-pipe-integration/src/main/java/androidx/camera/camera2/pipe/integration/config/UseCaseCameraConfig.kt
index b920ee1..f4441b5 100644
--- a/camera/camera-camera2-pipe-integration/src/main/java/androidx/camera/camera2/pipe/integration/config/UseCaseCameraConfig.kt
+++ b/camera/camera-camera2-pipe-integration/src/main/java/androidx/camera/camera2/pipe/integration/config/UseCaseCameraConfig.kt
@@ -31,6 +31,7 @@
 import androidx.camera.camera2.pipe.integration.adapter.SessionConfigAdapter
 import androidx.camera.camera2.pipe.integration.adapter.SessionConfigAdapter.Companion.toCamera2ImplConfig
 import androidx.camera.camera2.pipe.integration.impl.CameraCallbackMap
+import androidx.camera.camera2.pipe.integration.impl.CameraInteropStateCallbackRepository
 import androidx.camera.camera2.pipe.integration.impl.CapturePipelineImpl
 import androidx.camera.camera2.pipe.integration.impl.ComboRequestListener
 import androidx.camera.camera2.pipe.integration.impl.UseCaseCamera
@@ -85,6 +86,7 @@
         cameraPipe: CameraPipe,
         requestListener: ComboRequestListener,
         useCaseSurfaceManager: UseCaseSurfaceManager,
+        cameraInteropStateCallbackRepository: CameraInteropStateCallbackRepository
     ): UseCaseGraphConfig {
         val streamConfigMap = mutableMapOf<CameraStream.Config, DeferrableSurface>()
 
@@ -92,6 +94,7 @@
         //  imageReader or surface.
         val sessionConfigAdapter = SessionConfigAdapter(useCases)
         sessionConfigAdapter.getValidSessionConfigOrNull()?.let { sessionConfig ->
+            cameraInteropStateCallbackRepository.updateCallbacks(sessionConfig)
             sessionConfig.surfaces.forEach { deferrableSurface ->
                 val outputConfig = CameraStream.Config.create(
                     streamUseCase = getStreamUseCase(
diff --git a/camera/camera-camera2-pipe-integration/src/main/java/androidx/camera/camera2/pipe/integration/impl/CameraInteropStateCallbackRepository.kt b/camera/camera-camera2-pipe-integration/src/main/java/androidx/camera/camera2/pipe/integration/impl/CameraInteropStateCallbackRepository.kt
new file mode 100644
index 0000000..179fcb0
--- /dev/null
+++ b/camera/camera-camera2-pipe-integration/src/main/java/androidx/camera/camera2/pipe/integration/impl/CameraInteropStateCallbackRepository.kt
@@ -0,0 +1,180 @@
+/*
+ * 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.camera2.pipe.integration.impl
+
+import android.hardware.camera2.CameraCaptureSession
+import android.hardware.camera2.CameraDevice
+import android.os.Build
+import android.view.Surface
+import androidx.annotation.DoNotInline
+import androidx.annotation.RequiresApi
+import androidx.camera.camera2.pipe.core.Log
+import androidx.camera.core.impl.SessionConfig
+import javax.inject.Inject
+import javax.inject.Singleton
+import kotlinx.atomicfu.AtomicRef
+import kotlinx.atomicfu.atomic
+
+@RequiresApi(21) // TODO(b/200306659): Remove and replace with annotation on package-info.java
+@Singleton
+/**
+ * A application-level single-instance repository for Camera Interop callbacks. It supplies
+ * camera-pipe with internal callbacks on CameraX initialization. During runtime, before a camera
+ * graph is created, CameraX updates these internal callbacks with Camera Interop callbacks so that
+ * they may be triggered in camera-pipe.
+ */
+class CameraInteropStateCallbackRepository @Inject constructor() {
+
+    private val _deviceStateCallback = CameraInteropDeviceStateCallback()
+    private val _sessionStateCallback = CameraInteropSessionStateCallback()
+
+    /**
+     * Called after merging all sessionConfigs from CameraX useCases and UseCases supplied by Camera
+     * Interop. If the Interop has any callbacks, they would be contained in the sessionConfig.
+     * CameraInteropStateCallbackRepository would store these callbacks to be triggered by
+     * camera-pipe.
+     *
+     * @param sessionConfig the final merged sessionConfig used to create camera graph
+     */
+    fun updateCallbacks(sessionConfig: SessionConfig) {
+        _deviceStateCallback.updateCallbacks(sessionConfig)
+        _sessionStateCallback.updateCallbacks(sessionConfig)
+    }
+
+    val deviceStateCallback
+        get() = _deviceStateCallback
+
+    val sessionStateCallback
+        get() = _sessionStateCallback
+
+    class CameraInteropDeviceStateCallback() : CameraDevice.StateCallback() {
+
+        private var callbacks: AtomicRef<List<CameraDevice.StateCallback>> = atomic(listOf())
+        internal fun updateCallbacks(sessionConfig: SessionConfig) {
+            callbacks.value = sessionConfig.deviceStateCallbacks.toList()
+        }
+
+        override fun onOpened(cameraDevice: CameraDevice) {
+            for (callback in callbacks.value) {
+                callback.onOpened(cameraDevice)
+            }
+        }
+
+        override fun onClosed(cameraDevice: CameraDevice) {
+            for (callback in callbacks.value) {
+                callback.onClosed(cameraDevice)
+            }
+        }
+
+        override fun onDisconnected(cameraDevice: CameraDevice) {
+            for (callback in callbacks.value) {
+                callback.onDisconnected(cameraDevice)
+            }
+        }
+
+        override fun onError(cameraDevice: CameraDevice, errorCode: Int) {
+            for (callback in callbacks.value) {
+                callback.onError(cameraDevice, errorCode)
+            }
+        }
+    }
+
+    class CameraInteropSessionStateCallback() : CameraCaptureSession.StateCallback() {
+
+        private var callbacks: AtomicRef<List<CameraCaptureSession.StateCallback>> =
+            atomic(listOf())
+
+        internal fun updateCallbacks(sessionConfig: SessionConfig) {
+            callbacks.value = sessionConfig.sessionStateCallbacks.toList()
+        }
+
+        override fun onConfigured(session: CameraCaptureSession) {
+            for (callback in callbacks.value) {
+                callback.onConfigured(session)
+            }
+        }
+
+        override fun onConfigureFailed(session: CameraCaptureSession) {
+            for (callback in callbacks.value) {
+                callback.onConfigureFailed(session)
+            }
+        }
+
+        override fun onReady(session: CameraCaptureSession) {
+            for (callback in callbacks.value) {
+                callback.onReady(session)
+            }
+        }
+
+        override fun onActive(session: CameraCaptureSession) {
+            for (callback in callbacks.value) {
+                callback.onActive(session)
+            }
+        }
+
+        override fun onCaptureQueueEmpty(session: CameraCaptureSession) {
+            if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
+                Api26CompatImpl.onCaptureQueueEmpty(session, callbacks)
+            } else {
+                Log.error { "onCaptureQueueEmpty called for unsupported OS version." }
+            }
+        }
+
+        override fun onClosed(session: CameraCaptureSession) {
+            for (callback in callbacks.value) {
+                callback.onClosed(session)
+            }
+        }
+
+        override fun onSurfacePrepared(session: CameraCaptureSession, surface: Surface) {
+            if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
+                Api23CompatImpl.onSurfacePrepared(session, surface, callbacks)
+            } else {
+                Log.error { "onSurfacePrepared called for unsupported OS version." }
+            }
+        }
+
+        @RequiresApi(Build.VERSION_CODES.M)
+        private object Api23CompatImpl {
+            @DoNotInline
+            @JvmStatic
+            fun onSurfacePrepared(
+                session: CameraCaptureSession,
+                surface: Surface,
+                callbacks: AtomicRef<List<CameraCaptureSession.StateCallback>>
+            ) {
+                for (callback in callbacks.value) {
+                    callback.onSurfacePrepared(session, surface)
+                }
+            }
+        }
+
+        @RequiresApi(Build.VERSION_CODES.O)
+        private object Api26CompatImpl {
+            @DoNotInline
+            @JvmStatic
+            fun onCaptureQueueEmpty(
+                session: CameraCaptureSession,
+                callbacks: AtomicRef<List<CameraCaptureSession.StateCallback>>
+            ) {
+                for (callback in callbacks.value) {
+                    callback.onCaptureQueueEmpty(session)
+                }
+            }
+        }
+    }
+}
\ No newline at end of file
diff --git a/camera/camera-camera2-pipe-integration/src/test/java/androidx/camera/camera2/pipe/integration/adapter/FocusMeteringControlTest.kt b/camera/camera-camera2-pipe-integration/src/test/java/androidx/camera/camera2/pipe/integration/adapter/FocusMeteringControlTest.kt
index 2cb7051..98e6e74 100644
--- a/camera/camera-camera2-pipe-integration/src/test/java/androidx/camera/camera2/pipe/integration/adapter/FocusMeteringControlTest.kt
+++ b/camera/camera-camera2-pipe-integration/src/test/java/androidx/camera/camera2/pipe/integration/adapter/FocusMeteringControlTest.kt
@@ -353,6 +353,7 @@
         }
     }
 
+    @Ignore("b/266123157")
     @Test
     fun startFocusAndMetering_multiplePoints_3ARectsAreCorrect() = runBlocking {
         // Camera 0 i.e. Max AF count = 3, Max AE count = 3, Max AWB count = 1
@@ -885,6 +886,7 @@
         assertThat(focusMeteringControl.isFocusMeteringSupported(action)).isTrue()
     }
 
+    @Ignore("b/266123157")
     @Test
     fun isFocusMeteringSupported_noSupport3ARegion_shouldReturnFalse() {
         val action = FocusMeteringAction.Builder(point1).build()
diff --git a/camera/camera-camera2/src/test/java/androidx/camera/camera2/internal/CameraStateMachineTest.kt b/camera/camera-camera2/src/test/java/androidx/camera/camera2/internal/CameraStateMachineTest.kt
index b8d9b49..12ffb92 100644
--- a/camera/camera-camera2/src/test/java/androidx/camera/camera2/internal/CameraStateMachineTest.kt
+++ b/camera/camera-camera2/src/test/java/androidx/camera/camera2/internal/CameraStateMachineTest.kt
@@ -171,10 +171,8 @@
         private val states = mutableListOf<CameraState>()
         private var index = 0
 
-        override fun onChanged(state: CameraState?) {
-            if (state != null) {
-                states.add(state)
-            }
+        override fun onChanged(value: CameraState) {
+            states.add(value)
         }
 
         fun assertHasState(expectedState: CameraState): StateObserver {
diff --git a/camera/camera-extensions/src/androidTest/java/androidx/camera/extensions/PreviewTest.kt b/camera/camera-extensions/src/androidTest/java/androidx/camera/extensions/PreviewTest.kt
index 0896f12..f3a193e 100644
--- a/camera/camera-extensions/src/androidTest/java/androidx/camera/extensions/PreviewTest.kt
+++ b/camera/camera-extensions/src/androidTest/java/androidx/camera/extensions/PreviewTest.kt
@@ -44,6 +44,7 @@
 import org.junit.After
 import org.junit.Assume.assumeTrue
 import org.junit.Before
+import org.junit.Ignore
 import org.junit.Rule
 import org.junit.Test
 import org.junit.runner.RunWith
@@ -155,6 +156,7 @@
             get() = ExtensionsTestUtil.getAllExtensionsLensFacingCombinations()
     }
 
+    @Ignore("b/265988873")
     @UiThreadTest
     @Test
     fun canBindToLifeCycleAndDisplayPreview(): Unit = runBlocking {
diff --git a/camera/camera-extensions/src/androidTest/java/androidx/camera/extensions/internal/sessionprocessor/BasicExtenderSessionProcessorTest.kt b/camera/camera-extensions/src/androidTest/java/androidx/camera/extensions/internal/sessionprocessor/BasicExtenderSessionProcessorTest.kt
index fddf7c4..46e04fa 100644
--- a/camera/camera-extensions/src/androidTest/java/androidx/camera/extensions/internal/sessionprocessor/BasicExtenderSessionProcessorTest.kt
+++ b/camera/camera-extensions/src/androidTest/java/androidx/camera/extensions/internal/sessionprocessor/BasicExtenderSessionProcessorTest.kt
@@ -299,8 +299,8 @@
         val cameraClosedLatch = CountDownLatch(1)
         withContext(Dispatchers.Main) {
             camera.cameraInfo.cameraState.observeForever(object : Observer<CameraState?> {
-                override fun onChanged(cameraState: CameraState?) {
-                    if (cameraState?.type == CameraState.Type.CLOSED) {
+                override fun onChanged(value: CameraState?) {
+                    if (value?.type == CameraState.Type.CLOSED) {
                         cameraClosedLatch.countDown()
                         camera.cameraInfo.cameraState.removeObserver(this)
                     }
diff --git a/camera/camera-testing/src/main/java/androidx/camera/testing/fakes/FakeCameraDeviceSurfaceManager.java b/camera/camera-testing/src/main/java/androidx/camera/testing/fakes/FakeCameraDeviceSurfaceManager.java
index 2f630d2..d8201f8 100644
--- a/camera/camera-testing/src/main/java/androidx/camera/testing/fakes/FakeCameraDeviceSurfaceManager.java
+++ b/camera/camera-testing/src/main/java/androidx/camera/testing/fakes/FakeCameraDeviceSurfaceManager.java
@@ -16,6 +16,13 @@
 
 package androidx.camera.testing.fakes;
 
+import static android.graphics.ImageFormat.JPEG;
+import static android.graphics.ImageFormat.YUV_420_888;
+
+import static androidx.camera.core.impl.ImageFormatConstants.INTERNAL_DEFINED_IMAGE_FORMAT_PRIVATE;
+
+import static com.google.common.primitives.Ints.asList;
+
 import android.util.Size;
 
 import androidx.annotation.NonNull;
@@ -26,9 +33,12 @@
 import androidx.camera.core.impl.SurfaceConfig;
 import androidx.camera.core.impl.UseCaseConfig;
 
+import java.util.ArrayList;
 import java.util.HashMap;
+import java.util.HashSet;
 import java.util.List;
 import java.util.Map;
+import java.util.Set;
 
 /** A CameraDeviceSurfaceManager which has no supported SurfaceConfigs. */
 @RequiresApi(21) // TODO(b/200306659): Remove and replace with annotation on package-info.java
@@ -39,6 +49,8 @@
     private final Map<String, Map<Class<? extends UseCaseConfig<?>>, Size>> mDefinedResolutions =
             new HashMap<>();
 
+    private final Set<List<Integer>> mValidSurfaceCombos = createDefaultValidSurfaceCombos();
+
     /**
      * Sets the given suggested resolutions for the specified camera Id and use case type.
      */
@@ -77,6 +89,7 @@
             @NonNull String cameraId,
             @NonNull List<AttachedSurfaceInfo> existingSurfaces,
             @NonNull List<UseCaseConfig<?>> newUseCaseConfigs) {
+        checkSurfaceCombo(existingSurfaces, newUseCaseConfigs);
         Map<UseCaseConfig<?>, Size> suggestedSizes = new HashMap<>();
         for (UseCaseConfig<?> useCaseConfig : newUseCaseConfigs) {
             Size resolution = MAX_OUTPUT_SIZE;
@@ -94,4 +107,54 @@
 
         return suggestedSizes;
     }
+
+    /**
+     * Checks if the surface combinations is supported.
+     *
+     * <p> Throws {@link IllegalArgumentException} if not supported.
+     */
+    private void checkSurfaceCombo(List<AttachedSurfaceInfo> existingSurfaceInfos,
+            @NonNull List<UseCaseConfig<?>> newSurfaceConfigs) {
+        // Combine existing Surface with new Surface
+        List<Integer> currentCombo = new ArrayList<>();
+        for (UseCaseConfig<?> useCaseConfig : newSurfaceConfigs) {
+            currentCombo.add(useCaseConfig.getInputFormat());
+        }
+        for (AttachedSurfaceInfo surfaceInfo : existingSurfaceInfos) {
+            currentCombo.add(surfaceInfo.getImageFormat());
+        }
+        // Loop through valid combinations and return early if the combo is supported.
+        for (List<Integer> validCombo : mValidSurfaceCombos) {
+            if (isComboSupported(currentCombo, validCombo)) {
+                return;
+            }
+        }
+        // Throw IAE if none of the valid combos supports the current combo.
+        throw new IllegalArgumentException("Surface combo not supported");
+    }
+
+    /**
+     * Checks if the app combination in covered by the given valid combination.
+     */
+    private boolean isComboSupported(@NonNull List<Integer> appCombo,
+            @NonNull List<Integer> validCombo) {
+        List<Integer> combo = new ArrayList<>(validCombo);
+        for (Integer format : appCombo) {
+            if (!combo.remove(format)) {
+                return false;
+            }
+        }
+        return true;
+    }
+
+    /**
+     * The default combination is similar to LEGACY level devices.
+     */
+    private static Set<List<Integer>> createDefaultValidSurfaceCombos() {
+        Set<List<Integer>> validCombos = new HashSet<>();
+        validCombos.add(asList(INTERNAL_DEFINED_IMAGE_FORMAT_PRIVATE, YUV_420_888, JPEG));
+        validCombos.add(asList(INTERNAL_DEFINED_IMAGE_FORMAT_PRIVATE,
+                INTERNAL_DEFINED_IMAGE_FORMAT_PRIVATE));
+        return validCombos;
+    }
 }
diff --git a/camera/camera-testing/src/test/java/androidx/camera/testing/fakes/FakeCameraDeviceSurfaceManagerTest.java b/camera/camera-testing/src/test/java/androidx/camera/testing/fakes/FakeCameraDeviceSurfaceManagerTest.java
index 07528fb..c6b7329 100644
--- a/camera/camera-testing/src/test/java/androidx/camera/testing/fakes/FakeCameraDeviceSurfaceManagerTest.java
+++ b/camera/camera-testing/src/test/java/androidx/camera/testing/fakes/FakeCameraDeviceSurfaceManagerTest.java
@@ -16,13 +16,24 @@
 
 package androidx.camera.testing.fakes;
 
+import static android.graphics.ImageFormat.YUV_420_888;
+
+import static androidx.camera.core.impl.SurfaceConfig.ConfigSize.PREVIEW;
+import static androidx.camera.core.impl.SurfaceConfig.ConfigType.YUV;
+
 import static com.google.common.truth.Truth.assertThat;
 
-import static org.mockito.Mockito.mock;
+import static java.util.Arrays.asList;
+import static java.util.Collections.emptyList;
+import static java.util.Collections.singletonList;
 
 import android.os.Build;
+import android.util.Range;
 import android.util.Size;
 
+import androidx.camera.core.ImageAnalysis;
+import androidx.camera.core.impl.AttachedSurfaceInfo;
+import androidx.camera.core.impl.SurfaceConfig;
 import androidx.camera.core.impl.UseCaseConfig;
 
 import org.junit.Before;
@@ -36,6 +47,9 @@
 import java.util.List;
 import java.util.Map;
 
+/**
+ * Unit tests for {@link FakeCameraDeviceSurfaceManager}.
+ */
 @RunWith(RobolectricTestRunner.class)
 @DoNotInstrument
 @Config(minSdk = Build.VERSION_CODES.LOLLIPOP)
@@ -59,14 +73,44 @@
     @Before
     public void setUp() {
         mFakeCameraDeviceSurfaceManager = new FakeCameraDeviceSurfaceManager();
-        mFakeUseCaseConfig = mock(FakeUseCaseConfig.class);
+        mFakeUseCaseConfig = new FakeUseCaseConfig.Builder().getUseCaseConfig();
 
         mFakeCameraDeviceSurfaceManager.setSuggestedResolution(FAKE_CAMERA_ID0,
                 mFakeUseCaseConfig.getClass(), new Size(FAKE_WIDTH0, FAKE_HEIGHT0));
         mFakeCameraDeviceSurfaceManager.setSuggestedResolution(FAKE_CAMERA_ID1,
                 mFakeUseCaseConfig.getClass(), new Size(FAKE_WIDTH1, FAKE_HEIGHT1));
 
-        mUseCaseConfigList = Collections.singletonList((UseCaseConfig<?>) mFakeUseCaseConfig);
+        mUseCaseConfigList = singletonList(mFakeUseCaseConfig);
+    }
+
+    @Test
+    public void validSurfaceCombination_noException() {
+        UseCaseConfig<?> preview = new FakeUseCaseConfig.Builder().getUseCaseConfig();
+        UseCaseConfig<?> analysis = new ImageAnalysis.Builder().getUseCaseConfig();
+        mFakeCameraDeviceSurfaceManager.getSuggestedResolutions(FAKE_CAMERA_ID0,
+                emptyList(), asList(preview, analysis));
+    }
+
+    @Test(expected = IllegalArgumentException.class)
+    public void invalidSurfaceAndConfigCombination_throwException() {
+        UseCaseConfig<?> preview = new FakeUseCaseConfig.Builder().getUseCaseConfig();
+        UseCaseConfig<?> video = new FakeUseCaseConfig.Builder().getUseCaseConfig();
+        AttachedSurfaceInfo analysis = AttachedSurfaceInfo.create(
+                        SurfaceConfig.create(YUV, PREVIEW),
+                        YUV_420_888,
+                        new Size(1, 1),
+                        new Range<>(30, 30));
+        mFakeCameraDeviceSurfaceManager.getSuggestedResolutions(FAKE_CAMERA_ID0,
+                singletonList(analysis), asList(preview, video));
+    }
+
+    @Test(expected = IllegalArgumentException.class)
+    public void invalidConfigCombination_throwException() {
+        UseCaseConfig<?> preview = new FakeUseCaseConfig.Builder().getUseCaseConfig();
+        UseCaseConfig<?> video = new FakeUseCaseConfig.Builder().getUseCaseConfig();
+        UseCaseConfig<?> analysis = new ImageAnalysis.Builder().getUseCaseConfig();
+        mFakeCameraDeviceSurfaceManager.getSuggestedResolutions(FAKE_CAMERA_ID0,
+                Collections.emptyList(), asList(preview, video, analysis));
     }
 
     @Test
@@ -82,7 +126,6 @@
                 new Size(FAKE_WIDTH0, FAKE_HEIGHT0));
         assertThat(suggestedSizesCamera1.get(mFakeUseCaseConfig)).isEqualTo(
                 new Size(FAKE_WIDTH1, FAKE_HEIGHT1));
-
     }
 
 }
diff --git a/camera/integration-tests/coretestapp/src/androidTest/java/androidx/camera/integration/core/Camera2InteropIntegrationTest.kt b/camera/integration-tests/coretestapp/src/androidTest/java/androidx/camera/integration/core/Camera2InteropIntegrationTest.kt
index 7443347..812f339 100644
--- a/camera/integration-tests/coretestapp/src/androidTest/java/androidx/camera/integration/core/Camera2InteropIntegrationTest.kt
+++ b/camera/integration-tests/coretestapp/src/androidTest/java/androidx/camera/integration/core/Camera2InteropIntegrationTest.kt
@@ -34,6 +34,7 @@
 import androidx.camera.core.CameraXConfig
 import androidx.camera.core.ExtendableBuilder
 import androidx.camera.core.ImageAnalysis
+import androidx.camera.core.ImageCapture
 import androidx.camera.core.Preview
 import androidx.camera.core.impl.utils.executor.CameraXExecutors
 import androidx.camera.integration.core.util.CameraPipeUtil
@@ -105,9 +106,6 @@
 
     @Test
     fun cameraDeviceListener_receivesClose_afterUnbindAll(): Unit = runBlocking {
-        // Skips CameraPipe part now and will open this when camera-pipe-integration can support
-        Assume.assumeTrue(implName != CameraPipeConfig::class.simpleName)
-
         val previewBuilder = Preview.Builder()
         val deviceStateFlow = previewBuilder.createDeviceStateFlow()
 
@@ -131,6 +129,7 @@
                     unbindAllCalled = true
                     true // Filter out this state from the downstream flow
                 }
+
                 else -> false // Forward to the downstream flow
             }
         }.first()
@@ -140,6 +139,37 @@
     }
 
     @Test
+    fun cameraSessionListener_receivesClose_afterUnbindAll(): Unit = runBlocking {
+        val imageCaptureBuilder = ImageCapture.Builder()
+        val sessionStateFlow = imageCaptureBuilder.createSessionStateFlow()
+        withContext(Dispatchers.Main) {
+            processCameraProvider!!.bindToLifecycle(
+                TestLifecycleOwner(Lifecycle.State.RESUMED),
+                CameraSelector.DEFAULT_BACK_CAMERA,
+                imageCaptureBuilder.build()
+            )
+        }
+
+        var unbindAllCalled = false
+        val lastState = sessionStateFlow.dropWhile { state ->
+            when (state) {
+                // Filter out this state from the downstream flow
+                is SessionState.Unknown -> true
+                is SessionState.Configured -> {
+                    withContext(Dispatchers.Main) { processCameraProvider!!.unbindAll() }
+                    unbindAllCalled = true
+                    true // Filter out this state from the downstream flow
+                }
+
+                else -> false // Forward to the downstream flow
+            }
+        }.first()
+
+        assertThat(unbindAllCalled).isTrue()
+        assertThat(lastState).isEqualTo(SessionState.Ready)
+    }
+
+    @Test
     fun canUseCameraSelector_fromCamera2CameraIdAndCameraFilter(): Unit = runBlocking {
         val camera2CameraManager = ApplicationProvider.getApplicationContext<Context>()
             .getSystemService(CAMERA_SERVICE) as CameraManager
@@ -290,6 +320,15 @@
         data class Error(val errorCode: Int) : DeviceState()
     }
 
+    // Sealed class for converting CameraDevice.StateCallback into a StateFlow
+    sealed class SessionState {
+        object Unknown : SessionState()
+        object Ready : SessionState()
+        object Configured : SessionState()
+        object ConfigureFailed : SessionState()
+        object Closed : SessionState()
+    }
+
     /**
      * Returns a [StateFlow] which will signal the states of the camera defined in [DeviceState].
      */
@@ -320,6 +359,35 @@
             )
         }.asStateFlow()
 
+    /**
+     * Returns a [StateFlow] which will signal the states of the camera defined in [SessionState].
+     */
+    private fun <T> ExtendableBuilder<T>.createSessionStateFlow(): StateFlow<SessionState> =
+        MutableStateFlow<SessionState>(SessionState.Unknown).apply {
+            val stateCallback = object : CameraCaptureSession.StateCallback() {
+                override fun onReady(session: CameraCaptureSession) {
+                    tryEmit(SessionState.Ready)
+                }
+
+                override fun onConfigured(session: CameraCaptureSession) {
+                    tryEmit(SessionState.Configured)
+                }
+
+                override fun onConfigureFailed(session: CameraCaptureSession) {
+                    tryEmit(SessionState.ConfigureFailed)
+                }
+
+                override fun onClosed(session: CameraCaptureSession) {
+                    tryEmit(SessionState.Closed)
+                }
+            }
+            CameraPipeUtil.setSessionStateCallback(
+                implName,
+                this@createSessionStateFlow,
+                stateCallback
+            )
+        }.asStateFlow()
+
     private fun isBackwardCompatible(cameraManager: CameraManager, cameraId: String): Boolean {
         val cameraCharacteristics = cameraManager.getCameraCharacteristics(cameraId)
         val capabilities =
diff --git a/camera/integration-tests/coretestapp/src/androidTest/java/androidx/camera/integration/core/util/CameraPipeUtil.kt b/camera/integration-tests/coretestapp/src/androidTest/java/androidx/camera/integration/core/util/CameraPipeUtil.kt
index 0ecb83b..c89ede759 100644
--- a/camera/integration-tests/coretestapp/src/androidTest/java/androidx/camera/integration/core/util/CameraPipeUtil.kt
+++ b/camera/integration-tests/coretestapp/src/androidTest/java/androidx/camera/integration/core/util/CameraPipeUtil.kt
@@ -77,6 +77,25 @@
     )
     @OptIn(markerClass = [ExperimentalCamera2Interop::class])
     @JvmStatic
+    fun <T> setSessionStateCallback(
+        implName: String,
+        builder: ExtendableBuilder<T>,
+        stateCallback: CameraCaptureSession.StateCallback
+    ) {
+        if (implName == CameraPipeConfig::class.simpleName) {
+            androidx.camera.camera2.pipe.integration.interop.Camera2Interop.Extender(
+                builder
+            ).setSessionStateCallback(stateCallback)
+        } else {
+            Camera2Interop.Extender(builder).setSessionStateCallback(stateCallback)
+        }
+    }
+
+    @kotlin.OptIn(
+        androidx.camera.camera2.pipe.integration.interop.ExperimentalCamera2Interop::class
+    )
+    @OptIn(markerClass = [ExperimentalCamera2Interop::class])
+    @JvmStatic
     fun getCameraId(implName: String, cameraInfo: CameraInfo): String {
         return if (implName == CameraPipeConfig::class.simpleName) {
             androidx.camera.camera2.pipe.integration.interop.Camera2CameraInfo.from(
diff --git a/car/app/app/src/main/java/androidx/car/app/messaging/model/ConversationItem.java b/car/app/app/src/main/java/androidx/car/app/messaging/model/ConversationItem.java
index e4ba25d..ea41250 100644
--- a/car/app/app/src/main/java/androidx/car/app/messaging/model/ConversationItem.java
+++ b/car/app/app/src/main/java/androidx/car/app/messaging/model/ConversationItem.java
@@ -16,6 +16,8 @@
 
 package androidx.car.app.messaging.model;
 
+import static androidx.core.util.Preconditions.checkState;
+
 import static java.util.Objects.requireNonNull;
 
 import android.annotation.SuppressLint;
@@ -29,6 +31,7 @@
 import androidx.car.app.model.CarIcon;
 import androidx.car.app.model.CarText;
 import androidx.car.app.model.Item;
+import androidx.car.app.utils.CollectionUtils;
 
 import java.util.ArrayList;
 import java.util.List;
@@ -56,7 +59,8 @@
         this.mTitle = requireNonNull(builder.mTitle);
         this.mIcon = builder.mIcon;
         this.mIsGroupConversation = builder.mIsGroupConversation;
-        this.mMessages = requireNonNull(builder.mMessages);
+        this.mMessages = requireNonNull(CollectionUtils.unmodifiableCopy(builder.mMessages));
+        checkState(!mMessages.isEmpty(), "Message list cannot be empty.");
         this.mConversationCallbackDelegate = new ConversationCallbackDelegateImpl(
                 requireNonNull(builder.mConversationCallback));
     }
diff --git a/car/app/app/src/test/java/androidx/car/app/messaging/model/ConversationItemTest.java b/car/app/app/src/test/java/androidx/car/app/messaging/model/ConversationItemTest.java
new file mode 100644
index 0000000..dc250ea
--- /dev/null
+++ b/car/app/app/src/test/java/androidx/car/app/messaging/model/ConversationItemTest.java
@@ -0,0 +1,62 @@
+/*
+ * 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.car.app.messaging.model;
+
+import static org.junit.Assert.assertThrows;
+
+import androidx.car.app.model.CarIcon;
+
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.robolectric.RobolectricTestRunner;
+import org.robolectric.annotation.internal.DoNotInstrument;
+
+import java.util.ArrayList;
+
+/** Tests for {@link ConversationItem}. */
+@RunWith(RobolectricTestRunner.class)
+@DoNotInstrument
+public class ConversationItemTest {
+    /** Ensure the builder does not fail for the minimum set of required fields. */
+    @Test
+    public void build_withRequiredFieldsOnly() {
+        TestConversationFactory.createMinimalConversationItemBuilder().build();
+
+        // assert no crash
+    }
+
+    /** Ensure the builder does not fail when all fields are assigned. */
+    @Test
+    public void build_withAllFields() {
+        TestConversationFactory.createMinimalConversationItemBuilder()
+                .setIcon(CarIcon.APP_ICON) // icon is chosen arbitrarily for testing purposes
+                .setGroupConversation(true)
+                .build();
+
+        // assert no crash
+    }
+
+    @Test
+    public void build_throwsException_ifMessageListEmpty() {
+        assertThrows(
+                IllegalStateException.class,
+                () -> TestConversationFactory.createMinimalConversationItemBuilder()
+                        .setMessages(new ArrayList<>())
+                        .build()
+        );
+    }
+}
diff --git a/car/app/app/src/test/java/androidx/car/app/messaging/model/TestConversationFactory.java b/car/app/app/src/test/java/androidx/car/app/messaging/model/TestConversationFactory.java
new file mode 100644
index 0000000..8eb5a4a
--- /dev/null
+++ b/car/app/app/src/test/java/androidx/car/app/messaging/model/TestConversationFactory.java
@@ -0,0 +1,94 @@
+/*
+ * Copyright 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.car.app.messaging.model;
+
+import androidx.annotation.NonNull;
+import androidx.car.app.model.CarText;
+import androidx.car.app.model.ItemList;
+import androidx.core.app.Person;
+
+import java.util.ArrayList;
+import java.util.List;
+
+/** Factory for creating {@link ConversationItem} and related data in tests */
+public final class TestConversationFactory {
+    private static final ConversationCallback EMPTY_CONVERSATION_CALLBACK =
+            new ConversationCallback() {
+                @Override
+                public void onMarkAsRead() {
+                }
+
+                @Override
+                public void onTextReply(@NonNull String replyText) {
+                }
+            };
+
+    /**
+     * Creates a {@link Person} instance for testing
+     *
+     * <p>This method fills in the minimum required data to create a valid {@link Person}.
+     */
+    private static Person createMinimalPerson() {
+        return new Person.Builder().setName("Person Name").build();
+    }
+
+    /**
+     * Creates a {@link CarMessage} instance for testing
+     *
+     * <p>This method fills in the minimum required data to create a valid {@link CarMessage}.
+     */
+    private static CarMessage createMinimalMessage() {
+        return new CarMessage.Builder()
+                .setSender(createMinimalPerson())
+                .setBody(CarText.create("Message body"))
+                .build();
+    }
+
+    /**
+     * Creates a {@link ConversationItem.Builder} instance for testing
+     *
+     * <p>This method fills in the minimum required data to create a valid {@link
+     * ConversationItem.Builder}.
+     */
+    public static ConversationItem.Builder createMinimalConversationItemBuilder() {
+        List<CarMessage> messages = new ArrayList<>(1);
+        messages.add(createMinimalMessage());
+
+        return new ConversationItem.Builder()
+                .setId("conversation_id")
+                .setTitle(CarText.create("Conversation Title"))
+                .setMessages(messages)
+                .setConversationCallback(EMPTY_CONVERSATION_CALLBACK);
+    }
+
+    /**
+     * Creates a {@link ConversationItem} instance for testing
+     *
+     * <p>This method fills in the minimum required data to create a valid {@link ConversationItem}.
+     */
+    public static ConversationItem createMinimalConversationItem() {
+        return createMinimalConversationItemBuilder().build();
+    }
+
+    public static ItemList createItemListWithConversationItem() {
+        return new ItemList.Builder().addItem(createMinimalConversationItem()).build();
+    }
+
+    private TestConversationFactory() {
+        // Do not instantiate
+    }
+}
diff --git a/car/app/app/src/test/java/androidx/car/app/model/constraints/RowListConstraintsTest.java b/car/app/app/src/test/java/androidx/car/app/model/constraints/RowListConstraintsTest.java
index 8bb5fca..1ad7db1 100644
--- a/car/app/app/src/test/java/androidx/car/app/model/constraints/RowListConstraintsTest.java
+++ b/car/app/app/src/test/java/androidx/car/app/model/constraints/RowListConstraintsTest.java
@@ -18,11 +18,8 @@
 
 import static org.junit.Assert.assertThrows;
 
-import androidx.annotation.NonNull;
 import androidx.car.app.TestUtils;
-import androidx.car.app.messaging.model.ConversationCallback;
-import androidx.car.app.messaging.model.ConversationItem;
-import androidx.car.app.model.CarText;
+import androidx.car.app.messaging.model.TestConversationFactory;
 import androidx.car.app.model.ItemList;
 
 import org.junit.Test;
@@ -30,8 +27,6 @@
 import org.robolectric.RobolectricTestRunner;
 import org.robolectric.annotation.internal.DoNotInstrument;
 
-import java.util.ArrayList;
-
 /** Tests for {@link RowListConstraints}. */
 @RunWith(RobolectricTestRunner.class)
 @DoNotInstrument
@@ -94,25 +89,7 @@
     @Test
     public void validate_conversationItem_isAlwaysValid() {
         RowListConstraints constraints = new RowListConstraints.Builder().build();
-        ItemList itemList = new ItemList.Builder()
-                .addItem(new ConversationItem.Builder()
-                        .setId("id")
-                        .setTitle(CarText.create("title"))
-                        .setMessages(new ArrayList<>())
-                        .setConversationCallback(new ConversationCallback() {
-                            @Override
-                            public void onMarkAsRead() {
-                                // do nothing
-                            }
-
-                            @Override
-                            public void onTextReply(@NonNull String replyText) {
-                                // do nothing
-                            }
-                        })
-                        .build()
-                )
-                .build();
+        ItemList itemList = TestConversationFactory.createItemListWithConversationItem();
 
         constraints.validateOrThrow(itemList);
 
diff --git a/compose/animation/animation-core-lint/src/test/java/androidx/compose/animation/core/lint/TransitionDetectorTest.kt b/compose/animation/animation-core-lint/src/test/java/androidx/compose/animation/core/lint/TransitionDetectorTest.kt
index 268c145..d48f918 100644
--- a/compose/animation/animation-core-lint/src/test/java/androidx/compose/animation/core/lint/TransitionDetectorTest.kt
+++ b/compose/animation/animation-core-lint/src/test/java/androidx/compose/animation/core/lint/TransitionDetectorTest.kt
@@ -19,7 +19,7 @@
 package androidx.compose.animation.core.lint
 
 import androidx.compose.lint.test.Stubs
-import androidx.compose.lint.test.compiledStub
+import androidx.compose.lint.test.bytecodeStub
 import com.android.tools.lint.checks.infrastructure.LintDetectorTest
 import com.android.tools.lint.detector.api.Detector
 import com.android.tools.lint.detector.api.Issue
@@ -40,7 +40,7 @@
         mutableListOf(TransitionDetector.UnusedTransitionTargetStateParameter)
 
     // Simplified Transition.kt stubs
-    private val TransitionStub = compiledStub(
+    private val TransitionStub = bytecodeStub(
         filename = "Transition.kt",
         filepath = "androidx/compose/animation/core",
         checksum = 0x313a900e,
diff --git a/compose/animation/animation-core-lint/src/test/java/androidx/compose/animation/core/lint/UnrememberedAnimatableDetectorTest.kt b/compose/animation/animation-core-lint/src/test/java/androidx/compose/animation/core/lint/UnrememberedAnimatableDetectorTest.kt
index f18c818..c73c283 100644
--- a/compose/animation/animation-core-lint/src/test/java/androidx/compose/animation/core/lint/UnrememberedAnimatableDetectorTest.kt
+++ b/compose/animation/animation-core-lint/src/test/java/androidx/compose/animation/core/lint/UnrememberedAnimatableDetectorTest.kt
@@ -19,7 +19,7 @@
 package androidx.compose.animation.core.lint
 
 import androidx.compose.lint.test.Stubs
-import androidx.compose.lint.test.compiledStub
+import androidx.compose.lint.test.bytecodeStub
 import com.android.tools.lint.checks.infrastructure.LintDetectorTest
 import com.android.tools.lint.checks.infrastructure.TestMode
 import com.android.tools.lint.detector.api.Detector
@@ -41,7 +41,7 @@
         mutableListOf(UnrememberedAnimatableDetector.UnrememberedAnimatable)
 
     // Simplified Animatable Color function stub, from androidx.compose.animation
-    private val AnimatableColorStub = compiledStub(
+    private val AnimatableColorStub = bytecodeStub(
         filename = "SingleValueAnimation.kt",
         filepath = "androidx/compose/animation",
         checksum = 0x285b4455,
@@ -172,8 +172,9 @@
             AnimatableColorStub,
             Stubs.Color,
             Stubs.Composable,
-            Stubs.Remember
-        )
+            Stubs.Remember,
+            Stubs.SnapshotState
+            )
             .skipTestModes(TestMode.TYPE_ALIAS)
             .run()
             .expect(
@@ -336,7 +337,8 @@
             AnimatableColorStub,
             Stubs.Color,
             Stubs.Composable,
-            Stubs.Remember
+            Stubs.Remember,
+            Stubs.SnapshotState
         )
             .run()
             .expectClean()
@@ -449,8 +451,9 @@
             AnimatableColorStub,
             Stubs.Color,
             Stubs.Composable,
-            Stubs.Remember
-        )
+            Stubs.Remember,
+            Stubs.SnapshotState
+            )
             .run()
             .expectClean()
     }
diff --git a/compose/animation/animation-lint/src/test/java/androidx/compose/animation/lint/CrossfadeDetectorTest.kt b/compose/animation/animation-lint/src/test/java/androidx/compose/animation/lint/CrossfadeDetectorTest.kt
index 3cbd5de..d6d0b38 100644
--- a/compose/animation/animation-lint/src/test/java/androidx/compose/animation/lint/CrossfadeDetectorTest.kt
+++ b/compose/animation/animation-lint/src/test/java/androidx/compose/animation/lint/CrossfadeDetectorTest.kt
@@ -19,7 +19,7 @@
 package androidx.compose.animation.lint
 
 import androidx.compose.lint.test.Stubs
-import androidx.compose.lint.test.compiledStub
+import androidx.compose.lint.test.bytecodeStub
 import com.android.tools.lint.checks.infrastructure.LintDetectorTest
 import com.android.tools.lint.detector.api.Detector
 import com.android.tools.lint.detector.api.Issue
@@ -40,7 +40,7 @@
         mutableListOf(CrossfadeDetector.UnusedCrossfadeTargetStateParameter)
 
     // Simplified Transition.kt stubs
-    private val CrossfadeStub = compiledStub(
+    private val CrossfadeStub = bytecodeStub(
         filename = "Transition.kt",
         filepath = "androidx/compose/animation",
         checksum = 0x33cac1e3,
diff --git a/compose/compiler/compiler-hosted/integration-tests/src/test/java/androidx/compose/compiler/plugins/kotlin/AbstractCodegenSignatureTest.kt b/compose/compiler/compiler-hosted/integration-tests/src/test/java/androidx/compose/compiler/plugins/kotlin/AbstractCodegenSignatureTest.kt
index b763795..f0a2827 100644
--- a/compose/compiler/compiler-hosted/integration-tests/src/test/java/androidx/compose/compiler/plugins/kotlin/AbstractCodegenSignatureTest.kt
+++ b/compose/compiler/compiler-hosted/integration-tests/src/test/java/androidx/compose/compiler/plugins/kotlin/AbstractCodegenSignatureTest.kt
@@ -24,6 +24,7 @@
 import org.jetbrains.kotlin.backend.common.output.OutputFile
 import org.robolectric.Robolectric
 import java.net.URLClassLoader
+import org.junit.Assert.assertEquals
 
 fun printPublicApi(classDump: String, name: String): String {
     return classDump
@@ -60,27 +61,15 @@
 }
 
 abstract class AbstractCodegenSignatureTest : AbstractCodegenTest() {
-
-    private var isSetup = false
-    override fun setUp() {
-        isSetup = true
-        super.setUp()
-    }
-
-    private fun <T> ensureSetup(block: () -> T): T {
-        if (!isSetup) setUp()
-        return block()
-    }
-
     private fun OutputFile.printApi(): String {
         return printPublicApi(asText(), relativePath)
     }
 
-    fun checkApi(
+    protected fun checkApi(
         @Language("kotlin") src: String,
         expected: String,
         dumpClasses: Boolean = false
-    ): Unit = ensureSetup {
+    ) {
         val className = "Test_REPLACEME_${uniqueNumber++}"
         val fileName = "$className.kt"
 
@@ -96,8 +85,7 @@
         val apiString = loader
             .allGeneratedFiles
             .filter { it.relativePath.endsWith(".class") }
-            .map { it.printApi() }
-            .joinToString(separator = "\n")
+            .joinToString(separator = "\n") { it.printApi() }
             .replace(className, "Test")
 
         val expectedApiString = expected
@@ -109,10 +97,10 @@
         assertEquals(expectedApiString, apiString)
     }
 
-    fun checkComposerParam(
+    protected fun checkComposerParam(
         @Language("kotlin") src: String,
         dumpClasses: Boolean = false
-    ): Unit = ensureSetup {
+    ) {
         val className = "Test_REPLACEME_${uniqueNumber++}"
         val compiledClasses = classLoader(
             """
@@ -261,10 +249,10 @@
         }
     }
 
-    fun codegen(
+    protected fun codegen(
         @Language("kotlin") text: String,
         dumpClasses: Boolean = false
-    ): Unit = ensureSetup {
+    ) {
         codegenNoImports(
             """
            import android.content.Context
@@ -279,10 +267,10 @@
         )
     }
 
-    fun codegenNoImports(
+    private fun codegenNoImports(
         @Language("kotlin") text: String,
         dumpClasses: Boolean = false
-    ): Unit = ensureSetup {
+    ) {
         val className = "Test_${uniqueNumber++}"
         val fileName = "$className.kt"
 
diff --git a/compose/compiler/compiler-hosted/integration-tests/src/test/java/androidx/compose/compiler/plugins/kotlin/AbstractCodegenTest.kt b/compose/compiler/compiler-hosted/integration-tests/src/test/java/androidx/compose/compiler/plugins/kotlin/AbstractCodegenTest.kt
index 9fc894a..a514ee8 100644
--- a/compose/compiler/compiler-hosted/integration-tests/src/test/java/androidx/compose/compiler/plugins/kotlin/AbstractCodegenTest.kt
+++ b/compose/compiler/compiler-hosted/integration-tests/src/test/java/androidx/compose/compiler/plugins/kotlin/AbstractCodegenTest.kt
@@ -16,50 +16,13 @@
 
 package androidx.compose.compiler.plugins.kotlin
 
-import com.intellij.openapi.project.Project
-import com.intellij.openapi.util.io.FileUtil
-import com.intellij.openapi.util.text.StringUtilRt
-import com.intellij.openapi.vfs.CharsetToolkit
-import com.intellij.psi.PsiFileFactory
-import com.intellij.psi.impl.PsiFileFactoryImpl
-import com.intellij.testFramework.LightVirtualFile
-import org.intellij.lang.annotations.Language
-import org.jetbrains.kotlin.cli.jvm.compiler.EnvironmentConfigFiles
-import org.jetbrains.kotlin.cli.jvm.compiler.KotlinCoreEnvironment
-import org.jetbrains.kotlin.cli.jvm.config.addJvmClasspathRoots
-import org.jetbrains.kotlin.codegen.GeneratedClassLoader
-import org.jetbrains.kotlin.config.CompilerConfiguration
-import org.jetbrains.kotlin.config.JVMConfigurationKeys
-import org.jetbrains.kotlin.config.JvmTarget
-import org.jetbrains.kotlin.idea.KotlinLanguage
-import org.jetbrains.kotlin.psi.KtFile
-import org.jetbrains.kotlin.resolve.AnalyzingUtils
+import androidx.compose.compiler.plugins.kotlin.facade.SourceFile
 import java.io.File
-import org.jetbrains.kotlin.cli.jvm.config.configureJdkClasspathRoots
+import org.intellij.lang.annotations.Language
+import org.jetbrains.kotlin.codegen.GeneratedClassLoader
 
 abstract class AbstractCodegenTest : AbstractCompilerTest() {
-    override fun setUp() {
-        super.setUp()
-        val classPath = createClasspath() + additionalPaths
-
-        val configuration = newConfiguration()
-        configuration.addJvmClasspathRoots(classPath)
-        configuration.configureJdkClasspathRoots()
-        updateConfiguration(configuration)
-
-        myEnvironment = KotlinCoreEnvironment.createForTests(
-            myTestRootDisposable, configuration, EnvironmentConfigFiles.JVM_CONFIG_FILES
-        ).also { setupEnvironment(it) }
-    }
-
-    open fun updateConfiguration(configuration: CompilerConfiguration) {
-        configuration.put(JVMConfigurationKeys.IR, true)
-        configuration.put(JVMConfigurationKeys.JVM_TARGET, JvmTarget.JVM_1_8)
-    }
-
-    protected open fun helperFiles(): List<KtFile> = emptyList()
-
-    protected fun dumpClasses(loader: GeneratedClassLoader) {
+    private fun dumpClasses(loader: GeneratedClassLoader) {
         for (
             file in loader.allGeneratedFiles.filter {
                 it.relativePath.endsWith(".class")
@@ -75,7 +38,7 @@
         src: String,
         dumpClasses: Boolean = false,
         validate: (String) -> Unit
-    ): Unit = ensureSetup {
+    ) {
         val className = "Test_REPLACEME_${uniqueNumber++}"
         val fileName = "$className.kt"
 
@@ -97,10 +60,9 @@
 
         val apiString = loader
             .allGeneratedFiles
-            .filter { it.relativePath.endsWith(".class") }
-            .map {
+            .filter { it.relativePath.endsWith(".class") }.joinToString("\n") {
                 it.asText().replace('$', '%').replace(className, "Test")
-            }.joinToString("\n")
+            }
 
         validate(apiString)
     }
@@ -111,11 +73,7 @@
         fileName: String,
         dumpClasses: Boolean = false
     ): GeneratedClassLoader {
-        val files = mutableListOf<KtFile>()
-        files.addAll(helperFiles())
-        files.add(sourceFile(fileName, source))
-        myFiles = CodegenTestFiles.create(files)
-        val loader = createClassLoader()
+        val loader = createClassLoader(listOf(SourceFile(fileName, source)))
         if (dumpClasses) dumpClasses(loader)
         return loader
     }
@@ -124,37 +82,28 @@
         sources: Map<String, String>,
         dumpClasses: Boolean = false
     ): GeneratedClassLoader {
-        val files = mutableListOf<KtFile>()
-        files.addAll(helperFiles())
-        for ((fileName, source) in sources) {
-            files.add(sourceFile(fileName, source))
-        }
-        myFiles = CodegenTestFiles.create(files)
-        val loader = createClassLoader()
+        val loader = createClassLoader(
+            sources.map { (fileName, source) -> SourceFile(fileName, source) }
+        )
         if (dumpClasses) dumpClasses(loader)
         return loader
     }
 
-    protected fun testFile(source: String, dumpClasses: Boolean = false) {
-        val files = mutableListOf<KtFile>()
-        files.addAll(helperFiles())
-        files.add(sourceFile("Test.kt", source))
-        myFiles = CodegenTestFiles.create(files)
-        val loader = createClassLoader()
+    protected fun classLoader(
+        sources: Map<String, String>,
+        additionalPaths: List<File>,
+        dumpClasses: Boolean = false
+    ): GeneratedClassLoader {
+        val loader = createClassLoader(
+            sources.map { (fileName, source) -> SourceFile(fileName, source) },
+            additionalPaths
+        )
         if (dumpClasses) dumpClasses(loader)
-        val loadedClass = loader.loadClass("Test")
-        val instance = loadedClass.getDeclaredConstructor().newInstance()
-        val instanceClass = instance::class.java
-        val testMethod = instanceClass.getMethod("test")
-        testMethod.invoke(instance)
+        return loader
     }
 
     protected fun testCompile(source: String, dumpClasses: Boolean = false) {
-        val files = mutableListOf<KtFile>()
-        files.addAll(helperFiles())
-        files.add(sourceFile("Test.kt", source))
-        myFiles = CodegenTestFiles.create(files)
-        val loader = createClassLoader()
+        val loader = createClassLoader(listOf(SourceFile("Test.kt", source)))
         if (dumpClasses) dumpClasses(loader)
     }
 
@@ -230,45 +179,4 @@
         """,
             dumpClasses
         )
-
-    protected fun sourceFile(name: String, source: String): KtFile {
-        val result =
-            createFile(name, source, myEnvironment!!.project)
-        val ranges = AnalyzingUtils.getSyntaxErrorRanges(result)
-        assert(ranges.isEmpty()) { "Syntax errors found in $name: $ranges" }
-        return result
-    }
-
-    protected fun loadClass(className: String, source: String): Class<*> {
-        myFiles = CodegenTestFiles.create(
-            "file.kt",
-            source,
-            myEnvironment!!.project
-        )
-        val loader = createClassLoader()
-        return loader.loadClass(className)
-    }
-
-    protected open val additionalPaths = emptyList<File>()
-}
-
-fun createFile(name: String, text: String, project: Project): KtFile {
-    var shortName = name.substring(name.lastIndexOf('/') + 1)
-    shortName = shortName.substring(shortName.lastIndexOf('\\') + 1)
-    val virtualFile = object : LightVirtualFile(
-        shortName,
-        KotlinLanguage.INSTANCE,
-        StringUtilRt.convertLineSeparators(text)
-    ) {
-        override fun getPath(): String = "/$name"
-    }
-
-    virtualFile.setCharset(CharsetToolkit.UTF8_CHARSET)
-    val factory = PsiFileFactory.getInstance(project) as PsiFileFactoryImpl
-
-    return factory.trySetupPsiForFile(virtualFile, KotlinLanguage.INSTANCE, true, false) as KtFile
-}
-
-fun tmpDir(name: String): File {
-    return FileUtil.createTempDirectory(name, "", false).canonicalFile
 }
diff --git a/compose/compiler/compiler-hosted/integration-tests/src/test/java/androidx/compose/compiler/plugins/kotlin/AbstractCompilerTest.kt b/compose/compiler/compiler-hosted/integration-tests/src/test/java/androidx/compose/compiler/plugins/kotlin/AbstractCompilerTest.kt
index cef6022..b35ac1e 100644
--- a/compose/compiler/compiler-hosted/integration-tests/src/test/java/androidx/compose/compiler/plugins/kotlin/AbstractCompilerTest.kt
+++ b/compose/compiler/compiler-hosted/integration-tests/src/test/java/androidx/compose/compiler/plugins/kotlin/AbstractCompilerTest.kt
@@ -16,313 +16,137 @@
 
 package androidx.compose.compiler.plugins.kotlin
 
-import com.intellij.openapi.Disposable
+import androidx.compose.compiler.plugins.kotlin.facade.AnalysisResult
+import androidx.compose.compiler.plugins.kotlin.facade.KotlinCompilerFacade
+import androidx.compose.compiler.plugins.kotlin.facade.SourceFile
+import com.intellij.openapi.project.Project
 import com.intellij.openapi.util.Disposer
 import com.intellij.openapi.util.io.FileUtil
-import com.intellij.openapi.util.text.StringUtil
 import java.io.File
-import java.net.MalformedURLException
-import java.net.URL
 import java.net.URLClassLoader
-import junit.framework.TestCase
-import org.jetbrains.annotations.Contract
-import org.jetbrains.kotlin.cli.common.CLIConfigurationKeys
-import org.jetbrains.kotlin.cli.common.messages.CompilerMessageSeverity
-import org.jetbrains.kotlin.cli.common.messages.CompilerMessageSourceLocation
-import org.jetbrains.kotlin.cli.common.messages.IrMessageCollector
-import org.jetbrains.kotlin.cli.common.messages.MessageCollector
-import org.jetbrains.kotlin.cli.jvm.compiler.EnvironmentConfigFiles
-import org.jetbrains.kotlin.cli.jvm.compiler.KotlinCoreEnvironment
+import org.jetbrains.kotlin.backend.common.extensions.IrGenerationExtension
 import org.jetbrains.kotlin.cli.jvm.config.addJvmClasspathRoots
 import org.jetbrains.kotlin.cli.jvm.config.configureJdkClasspathRoots
-import org.jetbrains.kotlin.codegen.ClassFileFactory
 import org.jetbrains.kotlin.codegen.GeneratedClassLoader
-import org.jetbrains.kotlin.config.CommonConfigurationKeys
 import org.jetbrains.kotlin.config.CompilerConfiguration
 import org.jetbrains.kotlin.config.JVMConfigurationKeys
-import org.jetbrains.kotlin.ir.util.IrMessageLogger
-import org.jetbrains.kotlin.utils.rethrow
+import org.jetbrains.kotlin.ir.declarations.IrModuleFragment
 import org.junit.After
+import org.junit.BeforeClass
+import org.junit.runner.RunWith
+import org.junit.runners.JUnit4
 
-private const val KOTLIN_RUNTIME_VERSION = "1.3.11"
+@RunWith(JUnit4::class)
+abstract class AbstractCompilerTest {
+    companion object {
+        private fun File.applyExistenceCheck(): File = apply {
+            if (!exists()) throw NoSuchFileException(this)
+        }
 
-@Suppress("MemberVisibilityCanBePrivate")
-abstract class AbstractCompilerTest : TestCase() {
-    protected var myEnvironment: KotlinCoreEnvironment? = null
-    protected var myFiles: CodegenTestFiles? = null
-    protected var classFileFactory: ClassFileFactory? = null
-    protected var javaClassesOutputDirectory: File? = null
-    protected var additionalDependencies: List<File>? = null
+        private val homeDir: String = run {
+            val userDir = System.getProperty("user.dir")
+            val dir = File(userDir ?: ".")
+            val path = FileUtil.toCanonicalPath(dir.absolutePath)
+            File(path).applyExistenceCheck().absolutePath
+        }
 
-    override fun setUp() {
-        // Setup the environment for the analysis
-        System.setProperty(
-            "user.dir",
-            homeDir
-        )
-        myEnvironment = createEnvironment()
-        setupEnvironment(myEnvironment!!)
-        super.setUp()
+        private val projectRoot: String by lazy {
+            File(homeDir, "../../../../../..").applyExistenceCheck().absolutePath
+        }
+
+        val kotlinHome: File by lazy {
+            File(projectRoot, "prebuilts/androidx/external/org/jetbrains/kotlin/")
+                .applyExistenceCheck()
+        }
+
+        private val outDir: File by lazy {
+            File(System.getenv("OUT_DIR") ?: File(projectRoot, "out").absolutePath)
+                .applyExistenceCheck()
+        }
+
+        val composePluginJar: File by lazy {
+            File(outDir, "androidx/compose/compiler/compiler/build/repackaged/embedded.jar")
+                .applyExistenceCheck()
+        }
+
+        @JvmStatic
+        @BeforeClass
+        fun setSystemProperties() {
+            System.setProperty("idea.home", homeDir)
+            System.setProperty("user.dir", homeDir)
+            System.setProperty("idea.ignore.disabled.plugins", "true")
+        }
+
+        val defaultClassPath by lazy {
+            System.getProperty("java.class.path")!!.split(
+                System.getProperty("path.separator")!!
+            ).map { File(it) }
+        }
+
+        val defaultClassPathRoots by lazy {
+            defaultClassPath.filter {
+                !it.path.contains("robolectric") && it.extension != "xml"
+            }.toList()
+        }
     }
 
-    override fun tearDown() {
-        myFiles = null
-        myEnvironment = null
-        javaClassesOutputDirectory = null
-        additionalDependencies = null
-        classFileFactory = null
-        Disposer.dispose(myTestRootDisposable)
-        super.tearDown()
-    }
-
-    fun ensureSetup(block: () -> Unit) {
-        setUp()
-        block()
-    }
+    private val testRootDisposable = Disposer.newDisposable()
 
     @After
-    fun after() {
-        tearDown()
+    fun disposeTestRootDisposable() {
+        Disposer.dispose(testRootDisposable)
     }
 
-    protected val defaultClassPath by lazy { systemClassLoaderJars() }
+    protected open fun CompilerConfiguration.updateConfiguration() {}
 
-    protected fun createClasspath() = defaultClassPath.filter {
-        !it.path.contains("robolectric") && it.extension != "xml"
-    }.toList()
+    private fun createCompilerFacade(
+        additionalPaths: List<File> = listOf(),
+        registerExtensions: (Project.(CompilerConfiguration) -> Unit)? = null
+    ) = KotlinCompilerFacade.create(
+        testRootDisposable,
+        updateConfiguration = {
+            updateConfiguration()
+            addJvmClasspathRoots(additionalPaths)
+            addJvmClasspathRoots(defaultClassPathRoots)
+            if (!getBoolean(JVMConfigurationKeys.NO_JDK) &&
+                get(JVMConfigurationKeys.JDK_HOME) == null) {
+                // We need to set `JDK_HOME` explicitly to use JDK 17
+                put(JVMConfigurationKeys.JDK_HOME, File(System.getProperty("java.home")!!))
+            }
+            configureJdkClasspathRoots()
+        },
+        registerExtensions = registerExtensions ?: { configuration ->
+            ComposeComponentRegistrar.registerCommonExtensions(this)
+            IrGenerationExtension.registerExtension(
+                this,
+                ComposeComponentRegistrar.createComposeIrExtension(configuration)
+            )
+        }
+    )
 
-    val myTestRootDisposable = TestDisposable()
+    protected fun analyze(sourceFiles: List<SourceFile>): AnalysisResult =
+        createCompilerFacade().analyze(sourceFiles)
 
-    protected fun createEnvironment(): KotlinCoreEnvironment {
-        val classPath = createClasspath()
+    protected fun compileToIr(
+        sourceFiles: List<SourceFile>,
+        additionalPaths: List<File> = listOf(),
+        registerExtensions: (Project.(CompilerConfiguration) -> Unit)? = null
+    ): IrModuleFragment =
+        createCompilerFacade(additionalPaths, registerExtensions).compileToIr(sourceFiles)
 
-        val configuration = newConfiguration()
-        configuration.addJvmClasspathRoots(classPath)
-        configuration.configureJdkClasspathRoots()
-
-        System.setProperty("idea.ignore.disabled.plugins", "true")
-        return KotlinCoreEnvironment.createForTests(
-            myTestRootDisposable,
-            configuration,
-            EnvironmentConfigFiles.JVM_CONFIG_FILES
-        )
-    }
-
-    protected open fun setupEnvironment(environment: KotlinCoreEnvironment) {
-        ComposeComponentRegistrar.registerCommonExtensions(environment.project)
-        ComposeComponentRegistrar.registerIrExtension(
-            environment.project,
-            environment.configuration
-        )
-    }
-
-    protected fun createClassLoader(): GeneratedClassLoader {
+    protected fun createClassLoader(
+        sourceFiles: List<SourceFile>,
+        additionalPaths: List<File> = listOf()
+    ): GeneratedClassLoader {
         val classLoader = URLClassLoader(
-            defaultClassPath.map {
+            (additionalPaths + defaultClassPath).map {
                 it.toURI().toURL()
             }.toTypedArray(),
             null
         )
         return GeneratedClassLoader(
-            generateClassesInFile(),
-            classLoader,
-            *getClassPathURLs()
+            createCompilerFacade(additionalPaths).compile(sourceFiles).factory,
+            classLoader
         )
     }
-
-    protected fun getClassPathURLs(): Array<URL> {
-        val files = mutableListOf<File>()
-        javaClassesOutputDirectory?.let { files.add(it) }
-        additionalDependencies?.let { files.addAll(it) }
-
-        try {
-            return files.map { it.toURI().toURL() }.toTypedArray()
-        } catch (e: MalformedURLException) {
-            throw rethrow(e)
-        }
-    }
-
-    private fun reportProblem(e: Throwable) {
-        e.printStackTrace()
-        System.err.println("Generating instructions as text...")
-        try {
-            System.err.println(
-                classFileFactory?.createText()
-                    ?: "Cannot generate text: exception was thrown during generation"
-            )
-        } catch (e1: Throwable) {
-            System.err.println(
-                "Exception thrown while trying to generate text, " +
-                    "the actual exception follows:"
-            )
-            e1.printStackTrace()
-            System.err.println(
-                "------------------------------------------------------------------" +
-                    "-----------"
-            )
-        }
-
-        System.err.println("See exceptions above")
-    }
-
-    protected fun generateClassesInFile(reportProblems: Boolean = true): ClassFileFactory {
-        return classFileFactory ?: run {
-            try {
-                val environment = myEnvironment ?: error("Environment not initialized")
-                val files = myFiles ?: error("Files not initialized")
-                val generationState = GenerationUtils.compileFiles(environment, files.psiFiles)
-                generationState.factory.also { classFileFactory = it }
-            } catch (e: TestsCompilerError) {
-                if (reportProblems) {
-                    reportProblem(e.original)
-                } else {
-                    System.err.println("Compilation failure")
-                }
-                throw e
-            } catch (e: Throwable) {
-                if (reportProblems) reportProblem(e)
-                throw TestsCompilerError(e)
-            }
-        }
-    }
-
-    protected fun getTestName(lowercaseFirstLetter: Boolean): String =
-        getTestName(this.name ?: "", lowercaseFirstLetter)
-    protected fun getTestName(name: String, lowercaseFirstLetter: Boolean): String {
-        val trimmedName = trimStart(name, "test")
-        return if (StringUtil.isEmpty(trimmedName)) "" else lowercaseFirstLetter(
-            trimmedName,
-            lowercaseFirstLetter
-        )
-    }
-
-    protected fun lowercaseFirstLetter(name: String, lowercaseFirstLetter: Boolean): String =
-        if (lowercaseFirstLetter && !isMostlyUppercase(name))
-            Character.toLowerCase(name[0]) + name.substring(1)
-        else name
-
-    protected fun isMostlyUppercase(name: String): Boolean {
-        var uppercaseChars = 0
-        for (i in 0 until name.length) {
-            if (Character.isLowerCase(name[i])) {
-                return false
-            }
-            if (Character.isUpperCase(name[i])) {
-                uppercaseChars++
-                if (uppercaseChars >= 3) return true
-            }
-        }
-        return false
-    }
-
-    inner class TestDisposable : Disposable {
-
-        override fun dispose() {}
-
-        override fun toString(): String {
-            val testName = this@AbstractCompilerTest.getTestName(false)
-            return this@AbstractCompilerTest.javaClass.name +
-                if (StringUtil.isEmpty(testName)) "" else ".test$testName"
-        }
-    }
-
-    companion object {
-
-        private fun File.applyExistenceCheck(): File = this.apply {
-            if (!exists()) throw NoSuchFileException(this)
-        }
-
-        val homeDir by lazy { File(computeHomeDirectory()).applyExistenceCheck().absolutePath }
-        val projectRoot by lazy {
-            File(homeDir, "../../../../../..").applyExistenceCheck().absolutePath
-        }
-        val kotlinHome by lazy {
-            File(projectRoot, "prebuilts/androidx/external/org/jetbrains/kotlin/")
-                .applyExistenceCheck()
-        }
-        val outDir by lazy {
-            File(System.getenv("OUT_DIR") ?: File(projectRoot, "out").absolutePath)
-                .applyExistenceCheck()
-        }
-        val composePluginJar by lazy {
-            File(outDir, "androidx/compose/compiler/compiler/build/repackaged/embedded.jar")
-                .applyExistenceCheck()
-        }
-
-        fun kotlinRuntimeJar(module: String) = File(
-            kotlinHome, "$module/$KOTLIN_RUNTIME_VERSION/$module-$KOTLIN_RUNTIME_VERSION.jar"
-        ).applyExistenceCheck()
-
-        init {
-            System.setProperty(
-                "idea.home",
-                homeDir
-            )
-        }
-    }
-}
-
-private fun systemClassLoaderJars(): List<File> {
-    val classpath = System.getProperty("java.class.path")!!.split(
-        System.getProperty("path.separator")!!
-    )
-    val urls = classpath.map { URL("file://$it") }
-    val result = URLClassLoader(urls.toTypedArray()).urLs?.filter {
-        it.protocol == "file"
-    }?.map {
-        File(it.path)
-    }?.toList() ?: emptyList()
-    return result
-}
-
-private fun computeHomeDirectory(): String {
-    val userDir = System.getProperty("user.dir")
-    val dir = File(userDir ?: ".")
-    return FileUtil.toCanonicalPath(dir.absolutePath)
-}
-
-const val TEST_MODULE_NAME = "test-module"
-
-fun newConfiguration(): CompilerConfiguration {
-    val configuration = CompilerConfiguration()
-    configuration.put(
-        CommonConfigurationKeys.MODULE_NAME,
-        TEST_MODULE_NAME
-    )
-
-    configuration.put(JVMConfigurationKeys.VALIDATE_IR, true)
-
-    val messageCollector = object : MessageCollector {
-        override fun clear() {}
-
-        override fun report(
-            severity: CompilerMessageSeverity,
-            message: String,
-            location: CompilerMessageSourceLocation?
-        ) {
-            if (severity === CompilerMessageSeverity.ERROR) {
-                val prefix = if (location == null)
-                    ""
-                else
-                    "(" + location.path + ":" + location.line + ":" + location.column + ") "
-                throw AssertionError(prefix + message)
-            }
-        }
-
-        override fun hasErrors(): Boolean {
-            return false
-        }
-    }
-
-    configuration.put(CLIConfigurationKeys.MESSAGE_COLLECTOR_KEY, messageCollector)
-    configuration.put(IrMessageLogger.IR_MESSAGE_LOGGER, IrMessageCollector(messageCollector))
-
-    return configuration
-}
-
-@Contract(pure = true)
-fun trimStart(s: String, prefix: String): String {
-    return if (s.startsWith(prefix)) {
-        s.substring(prefix.length)
-    } else s
 }
diff --git a/compose/compiler/compiler-hosted/integration-tests/src/test/java/androidx/compose/compiler/plugins/kotlin/AbstractComposeDiagnosticsTest.kt b/compose/compiler/compiler-hosted/integration-tests/src/test/java/androidx/compose/compiler/plugins/kotlin/AbstractComposeDiagnosticsTest.kt
index 8806902..2cf385c 100644
--- a/compose/compiler/compiler-hosted/integration-tests/src/test/java/androidx/compose/compiler/plugins/kotlin/AbstractComposeDiagnosticsTest.kt
+++ b/compose/compiler/compiler-hosted/integration-tests/src/test/java/androidx/compose/compiler/plugins/kotlin/AbstractComposeDiagnosticsTest.kt
@@ -16,35 +16,26 @@
 
 package androidx.compose.compiler.plugins.kotlin
 
-import java.io.File
+import androidx.compose.compiler.plugins.kotlin.facade.AnalysisResult
+import androidx.compose.compiler.plugins.kotlin.facade.SourceFile
 import org.jetbrains.kotlin.checkers.DiagnosedRange
 import org.jetbrains.kotlin.checkers.utils.CheckerTestUtil
-import org.jetbrains.kotlin.cli.jvm.compiler.KotlinCoreEnvironment
-import org.jetbrains.kotlin.diagnostics.Diagnostic
+import org.junit.Assert
 
 abstract class AbstractComposeDiagnosticsTest : AbstractCompilerTest() {
+    private class DiagnosticTestException(message: String) : Exception(message)
 
-    fun doTest(expectedText: String) {
-        doTest(expectedText, myEnvironment!!)
-    }
-
-    fun doTest(expectedText: String, environment: KotlinCoreEnvironment) {
+    protected fun check(expectedText: String, ignoreParseErrors: Boolean = false) {
         val diagnosedRanges: MutableList<DiagnosedRange> = ArrayList()
         val clearText = CheckerTestUtil.parseDiagnosedRanges(expectedText, diagnosedRanges)
-        val file =
-            createFile("test.kt", clearText, environment.project)
-        val files = listOf(file)
 
-        // Use the JVM version of the analyzer to allow using classes in .jar files
-        val result = JvmResolveUtil.analyze(environment, files)
-
-        // Collect the errors
-        val errors = result.bindingContext.diagnostics.all().toMutableList()
-
-        val message = StringBuilder()
+        val errors = analyze(
+            listOf(SourceFile("test.kt", clearText, ignoreParseErrors))
+        ).diagnostics.toMutableList()
 
         // Ensure all the expected messages are there
-        val found = mutableSetOf<Diagnostic>()
+        val message = StringBuilder()
+        val found = mutableSetOf<AnalysisResult.Diagnostic>()
         for (range in diagnosedRanges) {
             for (diagnostic in range.getDiagnostics()) {
                 val reportedDiagnostics = errors.filter { it.factoryName == diagnostic.name }
@@ -59,15 +50,11 @@
                         val firstRange = reportedDiagnostics.first().textRanges.first()
                         message.append(
                             "  Error ${diagnostic.name} reported at ${
-                            firstRange.startOffset
+                                firstRange.startOffset
                             }-${firstRange.endOffset} but expected at ${range.start}-${range.end}\n"
                         )
                         message.append(
-                            sourceInfo(
-                                clearText,
-                                firstRange.startOffset, firstRange.endOffset,
-                                "  "
-                            )
+                            sourceInfo(clearText, firstRange.startOffset, firstRange.endOffset)
                         )
                     } else {
                         errors.remove(reportedDiagnostic)
@@ -76,16 +63,11 @@
                 } else {
                     message.append(
                         "  Diagnostic ${diagnostic.name} not reported, expected at ${
-                        range.start
+                            range.start
                         }\n"
                     )
                     message.append(
-                        sourceInfo(
-                            clearText,
-                            range.start,
-                            range.end,
-                            "  "
-                        )
+                        sourceInfo(clearText, range.start, range.end)
                     )
                 }
             }
@@ -97,46 +79,40 @@
                 val range = diagnostic.textRanges.first()
                 message.append(
                     "  Unexpected diagnostic ${diagnostic.factoryName} reported at ${
-                    range.startOffset
+                        range.startOffset
                     }\n"
                 )
                 message.append(
-                    sourceInfo(
-                        clearText,
-                        range.startOffset,
-                        range.endOffset,
-                        "  "
-                    )
+                    sourceInfo(clearText, range.startOffset, range.endOffset)
                 )
             }
         }
 
         // Throw an error if anything was found that was not expected
-        if (message.length > 0) throw Exception("Mismatched errors:\n$message")
+        if (message.isNotEmpty()) throw DiagnosticTestException("Mismatched errors:\n$message")
+    }
+
+    protected fun checkFail(expectedText: String) {
+        Assert.assertThrows(DiagnosticTestException::class.java) {
+            check(expectedText)
+        }
+    }
+
+    private fun String.lineStart(offset: Int): Int {
+        return this.lastIndexOf('\n', offset) + 1
+    }
+
+    private fun String.lineEnd(offset: Int): Int {
+        val result = this.indexOf('\n', offset)
+        return if (result < 0) this.length else result
+    }
+
+    // Return the source line that contains the given range with the range underlined with '~'s
+    private fun sourceInfo(clearText: String, start: Int, end: Int): String {
+        val lineStart = clearText.lineStart(start)
+        val lineEnd = clearText.lineEnd(start)
+        val displayEnd = if (end > lineEnd) lineEnd else end
+        return "  " + clearText.substring(lineStart, lineEnd) + "\n" +
+            " ".repeat(2 + start - lineStart) + "~".repeat(displayEnd - start) + "\n"
     }
 }
-
-fun assertExists(file: File): File {
-    if (!file.exists()) {
-        throw IllegalStateException("'$file' does not exist. Run test from gradle")
-    }
-    return file
-}
-
-fun String.lineStart(offset: Int): Int {
-    return this.lastIndexOf('\n', offset) + 1
-}
-
-fun String.lineEnd(offset: Int): Int {
-    val result = this.indexOf('\n', offset)
-    return if (result < 0) this.length else result
-}
-
-// Return the source line that contains the given range with the range underlined with '~'s
-fun sourceInfo(clearText: String, start: Int, end: Int, prefix: String = ""): String {
-    val lineStart = clearText.lineStart(start)
-    val lineEnd = clearText.lineEnd(start)
-    val displayEnd = if (end > lineEnd) lineEnd else end
-    return prefix + clearText.substring(lineStart, lineEnd) + "\n" +
-        prefix + " ".repeat(start - lineStart) + "~".repeat(displayEnd - start) + "\n"
-}
\ No newline at end of file
diff --git a/compose/compiler/compiler-hosted/integration-tests/src/test/java/androidx/compose/compiler/plugins/kotlin/AbstractIrTransformTest.kt b/compose/compiler/compiler-hosted/integration-tests/src/test/java/androidx/compose/compiler/plugins/kotlin/AbstractIrTransformTest.kt
index 594bc5d..167632a 100644
--- a/compose/compiler/compiler-hosted/integration-tests/src/test/java/androidx/compose/compiler/plugins/kotlin/AbstractIrTransformTest.kt
+++ b/compose/compiler/compiler-hosted/integration-tests/src/test/java/androidx/compose/compiler/plugins/kotlin/AbstractIrTransformTest.kt
@@ -16,52 +16,25 @@
 
 package androidx.compose.compiler.plugins.kotlin
 
+import androidx.compose.compiler.plugins.kotlin.facade.SourceFile
 import androidx.compose.compiler.plugins.kotlin.lower.dumpSrc
 import java.io.File
 import org.intellij.lang.annotations.Language
-import org.jetbrains.kotlin.backend.common.extensions.IrGenerationExtension
-import org.jetbrains.kotlin.backend.common.phaser.PhaseConfig
-import org.jetbrains.kotlin.backend.jvm.JvmIrCodegenFactory
-import org.jetbrains.kotlin.backend.jvm.jvmPhases
-import org.jetbrains.kotlin.cli.common.CLIConfigurationKeys
-import org.jetbrains.kotlin.cli.jvm.compiler.EnvironmentConfigFiles
-import org.jetbrains.kotlin.cli.jvm.compiler.KotlinCoreEnvironment
-import org.jetbrains.kotlin.cli.jvm.config.addJvmClasspathRoots
-import org.jetbrains.kotlin.cli.jvm.config.configureJdkClasspathRoots
-import org.jetbrains.kotlin.codegen.ClassBuilderFactories
-import org.jetbrains.kotlin.codegen.CodegenFactory
-import org.jetbrains.kotlin.codegen.state.GenerationState
 import org.jetbrains.kotlin.config.CompilerConfiguration
-import org.jetbrains.kotlin.config.JVMConfigurationKeys
-import org.jetbrains.kotlin.config.JvmTarget
 import org.jetbrains.kotlin.ir.IrElement
-import org.jetbrains.kotlin.ir.declarations.IrModuleFragment
 import org.jetbrains.kotlin.ir.util.dump
-import org.jetbrains.kotlin.psi.KtFile
+import org.junit.Assert.assertEquals
+import org.junit.Rule
+import org.junit.rules.TemporaryFolder
 
 abstract class AbstractIrTransformTest : AbstractCodegenTest() {
-    open val liveLiteralsEnabled get() = false
-    open val liveLiteralsV2Enabled get() = false
-    open val generateFunctionKeyMetaClasses get() = false
-    open val sourceInformationEnabled get() = true
-    open val intrinsicRememberEnabled get() = true
-    open val decoysEnabled get() = false
-    open val metricsDestination: String? get() = null
-    open val reportsDestination: String? get() = null
-    open val validateIr: Boolean get() = true
+    override fun CompilerConfiguration.updateConfiguration() {
+        put(ComposeConfiguration.SOURCE_INFORMATION_ENABLED_KEY, true)
+    }
 
-    protected fun createComposeIrGenerationExtension(): ComposeIrGenerationExtension =
-        ComposeIrGenerationExtension(
-            liveLiteralsEnabled,
-            liveLiteralsV2Enabled,
-            generateFunctionKeyMetaClasses,
-            sourceInformationEnabled,
-            intrinsicRememberEnabled,
-            decoysEnabled,
-            metricsDestination,
-            reportsDestination,
-            validateIr,
-        )
+    @JvmField
+    @Rule
+    val classesDirectory = TemporaryFolder()
 
     fun verifyCrossModuleComposeIrTransform(
         @Language("kotlin")
@@ -72,33 +45,22 @@
         dumpTree: Boolean = false,
         dumpClasses: Boolean = false,
     ) {
-        // Setup for compile
-        this.classFileFactory = null
-        this.myEnvironment = null
-        setUp()
-
         val dependencyFileName = "Test_REPLACEME_${uniqueNumber++}"
-        val classesDirectory = tmpDir("kotlin-classes")
 
         classLoader(dependencySource, dependencyFileName, dumpClasses)
             .allGeneratedFiles
             .also {
                 // Write the files to the class directory so they can be used by the next module
                 // and the application
-                it.writeToDir(classesDirectory)
+                it.writeToDir(classesDirectory.root)
             }
 
-        // Setup for compile
-        this.classFileFactory = null
-        this.myEnvironment = null
-        setUp()
-
         verifyComposeIrTransform(
             source,
             expectedTransformed,
             "",
             dumpTree = dumpTree,
-            additionalPaths = listOf(classesDirectory)
+            additionalPaths = listOf(classesDirectory.root)
         )
     }
 
@@ -112,13 +74,9 @@
         dumpTree: Boolean = false,
         truncateTracingInfoMode: TruncateTracingInfoMode = TruncateTracingInfoMode.TRUNCATE_KEY,
         additionalPaths: List<File> = listOf(),
-        applyExtraConfiguration: CompilerConfiguration.() -> Unit = {}
     ) {
-        val files = listOf(
-            sourceFile("Test.kt", source.replace('%', '$')),
-            sourceFile("Extra.kt", extra.replace('%', '$'))
-        )
-        val irModule = compileToIr(files, additionalPaths, applyExtraConfiguration)
+        val files = listOf(SourceFile("Test.kt", source), SourceFile("Extra.kt", extra))
+        val irModule = compileToIr(files, additionalPaths)
         val keySet = mutableListOf<Int>()
         fun IrElement.validate(): IrElement = this.also { validator(it) }
         val actualTransformed = irModule
@@ -313,60 +271,6 @@
         return result
     }
 
-    fun compileToIr(
-        files: List<KtFile>,
-        additionalPaths: List<File> = listOf(),
-        applyExtraConfiguration: CompilerConfiguration.() -> Unit = {}
-    ): IrModuleFragment = compileToIrWithExtension(
-        files, createComposeIrGenerationExtension(), additionalPaths, applyExtraConfiguration
-    )
-
-    fun compileToIrWithExtension(
-        files: List<KtFile>,
-        extension: IrGenerationExtension,
-        additionalPaths: List<File> = listOf(),
-        applyExtraConfiguration: CompilerConfiguration.() -> Unit = {}
-    ): IrModuleFragment {
-        val classPath = createClasspath() + additionalPaths
-        val configuration = newConfiguration()
-        configuration.addJvmClasspathRoots(classPath)
-        configuration.put(JVMConfigurationKeys.IR, true)
-        configuration.put(JVMConfigurationKeys.JVM_TARGET, JvmTarget.JVM_1_8)
-        configuration.applyExtraConfiguration()
-
-        configuration.configureJdkClasspathRoots()
-
-        val environment = KotlinCoreEnvironment.createForTests(
-            myTestRootDisposable, configuration, EnvironmentConfigFiles.JVM_CONFIG_FILES
-        )
-
-        ComposeComponentRegistrar.registerCommonExtensions(environment.project)
-        IrGenerationExtension.registerExtension(environment.project, extension)
-
-        val analysisResult = JvmResolveUtil.analyzeAndCheckForErrors(environment, files)
-        val codegenFactory = JvmIrCodegenFactory(
-            configuration,
-            configuration.get(CLIConfigurationKeys.PHASE_CONFIG) ?: PhaseConfig(jvmPhases)
-        )
-
-        val state = GenerationState.Builder(
-            environment.project,
-            ClassBuilderFactories.TEST,
-            analysisResult.moduleDescriptor,
-            analysisResult.bindingContext,
-            files,
-            configuration
-        ).isIrBackend(true).codegenFactory(codegenFactory).build()
-
-        state.beforeCompile()
-
-        val psi2irInput = CodegenFactory.IrConversionInput.fromGenerationStateAndFiles(
-            state,
-            files
-        )
-        return codegenFactory.convertToIr(psi2irInput).irModuleFragment
-    }
-
     enum class TruncateTracingInfoMode {
         TRUNCATE_KEY, // truncates only the `key` parameter
         KEEP_INFO_STRING, // truncates everything except for the `info` string
diff --git a/compose/compiler/compiler-hosted/integration-tests/src/test/java/androidx/compose/compiler/plugins/kotlin/AbstractLiveLiteralTransformTests.kt b/compose/compiler/compiler-hosted/integration-tests/src/test/java/androidx/compose/compiler/plugins/kotlin/AbstractLiveLiteralTransformTests.kt
index 335ab22..61a16f45 100644
--- a/compose/compiler/compiler-hosted/integration-tests/src/test/java/androidx/compose/compiler/plugins/kotlin/AbstractLiveLiteralTransformTests.kt
+++ b/compose/compiler/compiler-hosted/integration-tests/src/test/java/androidx/compose/compiler/plugins/kotlin/AbstractLiveLiteralTransformTests.kt
@@ -16,6 +16,7 @@
 
 package androidx.compose.compiler.plugins.kotlin
 
+import androidx.compose.compiler.plugins.kotlin.facade.SourceFile
 import androidx.compose.compiler.plugins.kotlin.lower.DurableKeyVisitor
 import androidx.compose.compiler.plugins.kotlin.lower.LiveLiteralTransformer
 import org.intellij.lang.annotations.Language
@@ -23,34 +24,43 @@
 import org.jetbrains.kotlin.backend.common.extensions.IrPluginContext
 import org.jetbrains.kotlin.ir.declarations.IrModuleFragment
 import org.jetbrains.kotlin.ir.util.DeepCopySymbolRemapper
-import org.jetbrains.kotlin.psi.KtFile
+import org.junit.Assert.assertEquals
 
 abstract class AbstractLiveLiteralTransformTests : AbstractIrTransformTest() {
-    private fun computeKeys(files: List<KtFile>): List<String> {
+    private fun computeKeys(files: List<SourceFile>): List<String> {
         var builtKeys = mutableSetOf<String>()
-        compileToIrWithExtension(
+        compileToIr(
             files,
-            object : IrGenerationExtension {
-                override fun generate(
-                    moduleFragment: IrModuleFragment,
-                    pluginContext: IrPluginContext
-                ) {
-                    val symbolRemapper = DeepCopySymbolRemapper()
-                    val keyVisitor = DurableKeyVisitor(builtKeys)
-                    val transformer = object : LiveLiteralTransformer(
-                        liveLiteralsEnabled || liveLiteralsV2Enabled,
-                        liveLiteralsV2Enabled,
-                        keyVisitor,
-                        pluginContext,
-                        symbolRemapper,
-                        ModuleMetricsImpl("temp")
+            registerExtensions = { configuration ->
+                val liveLiteralsEnabled = configuration.getBoolean(
+                    ComposeConfiguration.LIVE_LITERALS_ENABLED_KEY
+                )
+                val liveLiteralsV2Enabled = configuration.getBoolean(
+                    ComposeConfiguration.LIVE_LITERALS_V2_ENABLED_KEY
+                )
+                ComposeComponentRegistrar.registerCommonExtensions(this)
+                IrGenerationExtension.registerExtension(this, object : IrGenerationExtension {
+                    override fun generate(
+                        moduleFragment: IrModuleFragment,
+                        pluginContext: IrPluginContext
                     ) {
-                        override fun makeKeySet(): MutableSet<String> {
-                            return super.makeKeySet().also { builtKeys = it }
+                        val symbolRemapper = DeepCopySymbolRemapper()
+                        val keyVisitor = DurableKeyVisitor(builtKeys)
+                        val transformer = object : LiveLiteralTransformer(
+                            liveLiteralsEnabled || liveLiteralsV2Enabled,
+                            liveLiteralsV2Enabled,
+                            keyVisitor,
+                            pluginContext,
+                            symbolRemapper,
+                            ModuleMetricsImpl("temp")
+                        ) {
+                            override fun makeKeySet(): MutableSet<String> {
+                                return super.makeKeySet().also { builtKeys = it }
+                            }
                         }
+                        transformer.lower(moduleFragment)
                     }
-                    transformer.lower(moduleFragment)
-                }
+                })
             }
         )
         return builtKeys.toList()
@@ -59,20 +69,12 @@
     // since the lowering will throw an exception if duplicate keys are found, all we have to do
     // is run the lowering
     protected fun assertNoDuplicateKeys(@Language("kotlin") src: String) {
-        computeKeys(
-            listOf(
-                sourceFile("Test.kt", src.replace('%', '$'))
-            )
-        )
+        computeKeys(listOf(SourceFile("Test.kt", src)))
     }
 
     // For a given src string, a
     protected fun assertKeys(vararg keys: String, makeSrc: () -> String) {
-        val builtKeys = computeKeys(
-            listOf(
-                sourceFile("Test.kt", makeSrc().replace('%', '$'))
-            )
-        )
+        val builtKeys = computeKeys(listOf(SourceFile("Test.kt", makeSrc())))
         assertEquals(
             keys.toList().sorted().joinToString(separator = ",\n") {
                 "\"${it.replace('$', '%')}\""
@@ -85,17 +87,8 @@
 
     // test: have two src strings (before/after) and assert that the keys of the params didn't change
     protected fun assertDurableChange(before: String, after: String) {
-        val beforeKeys = computeKeys(
-            listOf(
-                sourceFile("Test.kt", before.replace('%', '$'))
-            )
-        )
-
-        val afterKeys = computeKeys(
-            listOf(
-                sourceFile("Test.kt", after.replace('%', '$'))
-            )
-        )
+        val beforeKeys = computeKeys(listOf(SourceFile("Test.kt", before)))
+        val afterKeys = computeKeys(listOf(SourceFile("Test.kt", after)))
 
         assertEquals(
             beforeKeys.toList().sorted().joinToString(separator = "\n"),
diff --git a/compose/compiler/compiler-hosted/integration-tests/src/test/java/androidx/compose/compiler/plugins/kotlin/AbstractMetricsTransformTest.kt b/compose/compiler/compiler-hosted/integration-tests/src/test/java/androidx/compose/compiler/plugins/kotlin/AbstractMetricsTransformTest.kt
index f1ed006..fbc7845 100644
--- a/compose/compiler/compiler-hosted/integration-tests/src/test/java/androidx/compose/compiler/plugins/kotlin/AbstractMetricsTransformTest.kt
+++ b/compose/compiler/compiler-hosted/integration-tests/src/test/java/androidx/compose/compiler/plugins/kotlin/AbstractMetricsTransformTest.kt
@@ -16,19 +16,27 @@
 
 package androidx.compose.compiler.plugins.kotlin
 
+import androidx.compose.compiler.plugins.kotlin.facade.KotlinCompilerFacade
+import androidx.compose.compiler.plugins.kotlin.facade.SourceFile
+import org.jetbrains.kotlin.backend.common.extensions.IrGenerationExtension
+import org.junit.Assert.assertEquals
+
 abstract class AbstractMetricsTransformTest : AbstractIrTransformTest() {
     private fun verifyMetrics(
         source: String,
         verify: ModuleMetrics.() -> Unit
     ) {
-        val files = listOf(
-            sourceFile("Test.kt", source.replace('%', '$')),
+        val files = listOf(SourceFile("Test.kt", source))
+        val metrics = ModuleMetricsImpl(KotlinCompilerFacade.TEST_MODULE_NAME)
+        compileToIr(
+            files,
+            registerExtensions = { configuration ->
+                ComposeComponentRegistrar.registerCommonExtensions(this)
+                val extension = ComposeComponentRegistrar.createComposeIrExtension(configuration)
+                extension.metrics = metrics
+                IrGenerationExtension.registerExtension(this, extension)
+            }
         )
-
-        val extension = createComposeIrGenerationExtension()
-        val metrics = ModuleMetricsImpl(TEST_MODULE_NAME)
-        extension.metrics = metrics
-        compileToIrWithExtension(files, extension)
         metrics.verify()
     }
 
diff --git a/compose/compiler/compiler-hosted/integration-tests/src/test/java/androidx/compose/compiler/plugins/kotlin/AbstractMultiPlatformIntegrationTest.kt b/compose/compiler/compiler-hosted/integration-tests/src/test/java/androidx/compose/compiler/plugins/kotlin/AbstractMultiPlatformIntegrationTest.kt
index 81a3040..6d470eb 100644
--- a/compose/compiler/compiler-hosted/integration-tests/src/test/java/androidx/compose/compiler/plugins/kotlin/AbstractMultiPlatformIntegrationTest.kt
+++ b/compose/compiler/compiler-hosted/integration-tests/src/test/java/androidx/compose/compiler/plugins/kotlin/AbstractMultiPlatformIntegrationTest.kt
@@ -28,6 +28,9 @@
 import java.io.File
 import java.io.PrintStream
 import java.io.PrintWriter
+import org.junit.Assert.assertEquals
+import org.junit.Rule
+import org.junit.rules.TemporaryFolder
 
 // AbstractCliTest
 private fun executeCompilerGrabOutput(
@@ -75,20 +78,20 @@
     }
 
 abstract class AbstractMultiPlatformIntegrationTest : AbstractCompilerTest() {
-    fun multiplatform(
+    @JvmField
+    @Rule
+    val sourceDirectory = TemporaryFolder()
+
+    protected fun multiplatform(
         @Language("kotlin")
         common: String,
         @Language("kotlin")
         jvm: String,
         output: String
     ) {
-        setUp()
-        val tmpdir = tmpDir(getTestName(true))
-
-        assert(
-            composePluginJar.exists(),
-            { "Compiler plugin jar does not exist: $composePluginJar" }
-        )
+        assert(composePluginJar.exists()) {
+            "Compiler plugin jar does not exist: $composePluginJar"
+        }
 
         val optionalArgs = arrayOf(
             "-cp",
@@ -96,21 +99,21 @@
                 .filter { it.exists() }
                 .joinToString(File.pathSeparator) { it.absolutePath },
             "-kotlin-home",
-            AbstractCompilerTest.kotlinHome.absolutePath,
+            kotlinHome.absolutePath,
             "-Xplugin=${composePluginJar.absolutePath}",
             "-Xuse-ir"
         )
 
         val jvmOnlyArgs = arrayOf("-no-stdlib")
 
-        val srcDir = File(tmpdir, "srcs").absolutePath
+        val srcDir = sourceDirectory.newFolder("srcs").absolutePath
         val commonSrc = File(srcDir, "common.kt")
         val jvmSrc = File(srcDir, "jvm.kt")
 
         FileUtil.writeToFile(commonSrc, common)
         FileUtil.writeToFile(jvmSrc, jvm)
 
-        val jvmDest = File(tmpdir, "jvm").absolutePath
+        val jvmDest = sourceDirectory.newFolder("jvm").absolutePath
 
         val result = K2JVMCompiler().compile(
             jvmSrc,
diff --git a/compose/compiler/compiler-hosted/integration-tests/src/test/java/androidx/compose/compiler/plugins/kotlin/ClassStabilityTransformTests.kt b/compose/compiler/compiler-hosted/integration-tests/src/test/java/androidx/compose/compiler/plugins/kotlin/ClassStabilityTransformTests.kt
index 46b70bb..f70db8d 100644
--- a/compose/compiler/compiler-hosted/integration-tests/src/test/java/androidx/compose/compiler/plugins/kotlin/ClassStabilityTransformTests.kt
+++ b/compose/compiler/compiler-hosted/integration-tests/src/test/java/androidx/compose/compiler/plugins/kotlin/ClassStabilityTransformTests.kt
@@ -17,6 +17,7 @@
 package androidx.compose.compiler.plugins.kotlin
 
 import androidx.compose.compiler.plugins.kotlin.analysis.stabilityOf
+import androidx.compose.compiler.plugins.kotlin.facade.SourceFile
 import org.intellij.lang.annotations.Language
 import org.jetbrains.kotlin.ir.declarations.IrClass
 import org.jetbrains.kotlin.ir.declarations.IrModuleFragment
@@ -26,10 +27,10 @@
 import org.jetbrains.kotlin.ir.types.IrType
 import org.jetbrains.kotlin.ir.util.defaultType
 import org.jetbrains.kotlin.ir.util.statements
+import org.junit.Assert.assertEquals
 import org.junit.Test
 
 class ClassStabilityTransformTests : AbstractIrTransformTest() {
-
     @Test
     fun testEmptyClassIsStable() = assertStability(
         "class Foo",
@@ -1495,9 +1496,7 @@
             class Unstable { var value: Int = 0 }
         """.trimIndent()
 
-        val files = listOf(
-            sourceFile("Test.kt", source.replace('%', '$'))
-        )
+        val files = listOf(SourceFile("Test.kt", source))
         val irModule = compileToIr(files)
         val irClass = irModule.files.last().declarations.first() as IrClass
         val classStability = stabilityOf(irClass.defaultType as IrType)
@@ -1570,11 +1569,6 @@
         localSrc: String,
         dumpClasses: Boolean = false
     ): IrModuleFragment {
-        // Setup for compile
-        this.classFileFactory = null
-        this.myEnvironment = null
-        setUp()
-
         val dependencyFileName = "Test_REPLACEME_${uniqueNumber++}"
         val dependencySrc = """
             package dependency
@@ -1591,20 +1585,14 @@
             $externalSrc
         """.trimIndent()
 
-        val classesDirectory = tmpDir("kotlin-classes")
         classLoader(dependencySrc, dependencyFileName, dumpClasses)
             .allGeneratedFiles
             .also {
                 // Write the files to the class directory so they can be used by the next module
                 // and the application
-                it.writeToDir(classesDirectory)
+                it.writeToDir(classesDirectory.root)
             }
 
-        // Setup for compile
-        this.classFileFactory = null
-        this.myEnvironment = null
-        setUp()
-
         val source = """
             import dependency.*
             import androidx.compose.runtime.mutableStateOf
@@ -1618,10 +1606,8 @@
             $localSrc
         """.trimIndent()
 
-        val files = listOf(
-            sourceFile("Test.kt", source.replace('%', '$'))
-        )
-        return compileToIr(files, additionalPaths = listOf(classesDirectory))
+        val files = listOf(SourceFile("Test.kt", source))
+        return compileToIr(files, listOf(classesDirectory.root))
     }
 
     private fun assertTransform(
diff --git a/compose/compiler/compiler-hosted/integration-tests/src/test/java/androidx/compose/compiler/plugins/kotlin/CodegenMetadataTests.kt b/compose/compiler/compiler-hosted/integration-tests/src/test/java/androidx/compose/compiler/plugins/kotlin/CodegenMetadataTests.kt
index bfdaddc..9f65d26 100644
--- a/compose/compiler/compiler-hosted/integration-tests/src/test/java/androidx/compose/compiler/plugins/kotlin/CodegenMetadataTests.kt
+++ b/compose/compiler/compiler-hosted/integration-tests/src/test/java/androidx/compose/compiler/plugins/kotlin/CodegenMetadataTests.kt
@@ -20,14 +20,12 @@
 import org.junit.Test
 
 class CodegenMetadataTests : AbstractLoweringTests() {
-
-    override fun updateConfiguration(configuration: CompilerConfiguration) {
-        super.updateConfiguration(configuration)
-        configuration.put(ComposeConfiguration.LIVE_LITERALS_ENABLED_KEY, true)
+    override fun CompilerConfiguration.updateConfiguration() {
+        put(ComposeConfiguration.LIVE_LITERALS_ENABLED_KEY, true)
     }
 
     @Test
-    fun testBasicFunctionality(): Unit = ensureSetup {
+    fun testBasicFunctionality() {
         val className = "Test_${uniqueNumber++}"
         val fileName = "$className.kt"
         val loader = classLoader(
diff --git a/compose/compiler/compiler-hosted/integration-tests/src/test/java/androidx/compose/compiler/plugins/kotlin/CodegenTestFiles.kt b/compose/compiler/compiler-hosted/integration-tests/src/test/java/androidx/compose/compiler/plugins/kotlin/CodegenTestFiles.kt
deleted file mode 100644
index 44424fe..0000000
--- a/compose/compiler/compiler-hosted/integration-tests/src/test/java/androidx/compose/compiler/plugins/kotlin/CodegenTestFiles.kt
+++ /dev/null
@@ -1,49 +0,0 @@
-/*
- * Copyright 2019 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- *      http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-package androidx.compose.compiler.plugins.kotlin
-
-import com.intellij.openapi.project.Project
-import org.jetbrains.kotlin.checkers.utils.CheckerTestUtil
-import org.jetbrains.kotlin.psi.KtFile
-
-class CodegenTestFiles private constructor(
-    val psiFiles: List<KtFile>
-) {
-
-    companion object {
-        fun create(ktFiles: List<KtFile>): CodegenTestFiles {
-            assert(!ktFiles.isEmpty()) { "List should have at least one file" }
-            return CodegenTestFiles(ktFiles)
-        }
-
-        fun create(
-            fileName: String,
-            contentWithDiagnosticMarkup: String,
-            project: Project
-        ): CodegenTestFiles {
-            val content = CheckerTestUtil.parseDiagnosedRanges(
-                contentWithDiagnosticMarkup,
-                ArrayList(),
-                null
-            )
-
-            val file = createFile(fileName, content, project)
-
-            return create(listOf(file))
-        }
-    }
-}
diff --git a/compose/compiler/compiler-hosted/integration-tests/src/test/java/androidx/compose/compiler/plugins/kotlin/ComposeCallLoweringTests.kt b/compose/compiler/compiler-hosted/integration-tests/src/test/java/androidx/compose/compiler/plugins/kotlin/ComposeCallLoweringTests.kt
index d1ad863..c83cb3e 100644
--- a/compose/compiler/compiler-hosted/integration-tests/src/test/java/androidx/compose/compiler/plugins/kotlin/ComposeCallLoweringTests.kt
+++ b/compose/compiler/compiler-hosted/integration-tests/src/test/java/androidx/compose/compiler/plugins/kotlin/ComposeCallLoweringTests.kt
@@ -20,14 +20,14 @@
 import android.widget.Button
 import android.widget.LinearLayout
 import android.widget.TextView
-import com.intellij.psi.PsiElement
-import com.intellij.psi.util.PsiTreeUtil
+import org.junit.Assert.assertEquals
+import org.junit.Assert.assertNotNull
+import org.junit.Assert.assertNull
 import org.junit.Ignore
 import org.junit.Test
 import org.junit.runner.RunWith
 import org.robolectric.RobolectricTestRunner
 import org.robolectric.annotation.Config
-import kotlin.reflect.KClass
 
 @RunWith(RobolectricTestRunner::class)
 @Config(
@@ -36,10 +36,9 @@
     maxSdk = 23
 )
 class ComposeCallLoweringTests : AbstractLoweringTests() {
-
     @Test
     @Ignore("b/173733968")
-    fun testInlineGroups(): Unit = ensureSetup {
+    fun testInlineGroups() {
         compose(
             """
 
@@ -63,7 +62,7 @@
 
     @Test
     @Ignore("b/173733968")
-    fun testReturnInsideKey(): Unit = ensureSetup {
+    fun testReturnInsideKey() {
         compose(
             """
             @Composable fun ShowMessage(text: String): Int = key(text) {
@@ -84,7 +83,7 @@
 
     @Test
     @Ignore("b/173733968")
-    fun testMoveFromIssue(): Unit = ensureSetup {
+    fun testMoveFromIssue() {
         compose(
             """
         """,
@@ -97,7 +96,7 @@
 
     @Test
     @Ignore("b/173733968")
-    fun testSimpleInlining(): Unit = ensureSetup {
+    fun testSimpleInlining() {
         compose(
             """
             @Composable
@@ -116,7 +115,7 @@
 
     @Test
     @Ignore("b/173733968")
-    fun testVarargCall(): Unit = ensureSetup {
+    fun testVarargCall() {
         compose(
             """
             @Composable
@@ -146,7 +145,7 @@
 
     @Test
     @Ignore("b/173733968")
-    fun testVarargs(): Unit = ensureSetup {
+    fun testVarargs() {
         codegen(
             """
             import androidx.compose.runtime.*
@@ -168,7 +167,7 @@
 
     @Test
     @Ignore("b/173733968")
-    fun testComposableLambdaCall(): Unit = ensureSetup {
+    fun testComposableLambdaCall() {
         codegen(
             """
                 import androidx.compose.runtime.*
@@ -183,7 +182,7 @@
 
     @Test
     @Ignore("b/173733968")
-    fun testProperties(): Unit = ensureSetup {
+    fun testProperties() {
         codegen(
             """
             import androidx.compose.runtime.*
@@ -213,7 +212,7 @@
 
     @Test
     @Ignore("b/173733968")
-    fun testUnboundSymbolIssue(): Unit = ensureSetup {
+    fun testUnboundSymbolIssue() {
         codegenNoImports(
             """
             import androidx.compose.runtime.Composable
@@ -268,7 +267,7 @@
 
     @Test
     @Ignore("b/173733968")
-    fun testPropertyValues(): Unit = ensureSetup {
+    fun testPropertyValues() {
         compose(
             """
             val foo @Composable get() = "123"
@@ -301,7 +300,7 @@
 
     @Test
     @Ignore("b/173733968")
-    fun testComposableLambdaCallWithGenerics(): Unit = ensureSetup {
+    fun testComposableLambdaCallWithGenerics() {
         codegen(
             """
                 import androidx.compose.runtime.*
@@ -332,7 +331,7 @@
 
     @Test
     @Ignore("b/173733968")
-    fun testMethodInvocations(): Unit = ensureSetup {
+    fun testMethodInvocations() {
         codegen(
             """
                 import androidx.compose.runtime.*
@@ -351,7 +350,7 @@
 
     @Test
     @Ignore("b/173733968")
-    fun testReceiverLambdaInvocation(): Unit = ensureSetup {
+    fun testReceiverLambdaInvocation() {
         codegen(
             """
                 class TextSpanScope
@@ -365,7 +364,7 @@
 
     @Test
     @Ignore("b/173733968")
-    fun testReceiverLambda2(): Unit = ensureSetup {
+    fun testReceiverLambda2() {
         codegen(
             """
                 class DensityScope(val density: Density)
@@ -387,7 +386,7 @@
 
     @Test
     @Ignore("b/173733968")
-    fun testInlineChildren(): Unit = ensureSetup {
+    fun testInlineChildren() {
         codegen(
             """
                 import androidx.compose.runtime.*
@@ -407,7 +406,7 @@
 
     @Test
     @Ignore("b/173733968")
-    fun testNoComposerImport(): Unit = ensureSetup {
+    fun testNoComposerImport() {
         codegenNoImports(
             """
         import androidx.compose.runtime.Composable
@@ -432,7 +431,7 @@
 
     @Test
     @Ignore("b/173733968")
-    fun testInlineNoinline(): Unit = ensureSetup {
+    fun testInlineNoinline() {
         codegen(
             """
         @Composable
@@ -456,7 +455,7 @@
 
     @Test
     @Ignore("b/173733968")
-    fun testInlinedComposable(): Unit = ensureSetup {
+    fun testInlinedComposable() {
         codegen(
             """
         @Composable
@@ -476,7 +475,7 @@
 
     @Test
     @Ignore("b/173733968")
-    fun testGenericParameterOrderIssue(): Unit = ensureSetup {
+    fun testGenericParameterOrderIssue() {
         codegen(
             """
 @Composable
@@ -495,7 +494,7 @@
 
     @Test
     @Ignore("b/173733968")
-    fun testArgumentOrderIssue(): Unit = ensureSetup {
+    fun testArgumentOrderIssue() {
         codegen(
             """
                 class A
@@ -518,7 +517,7 @@
 
     @Test
     @Ignore("b/173733968")
-    fun testObjectName(): Unit = ensureSetup {
+    fun testObjectName() {
         codegen(
             """
 
@@ -536,7 +535,7 @@
 
     @Test
     @Ignore("b/173733968")
-    fun testStuffThatIWantTo(): Unit = ensureSetup {
+    fun testStuffThatIWantTo() {
         codegen(
             """
 
@@ -555,7 +554,7 @@
 
     @Test
     @Ignore("b/173733968")
-    fun testSimpleFunctionResolution(): Unit = ensureSetup {
+    fun testSimpleFunctionResolution() {
         compose(
             """
             import androidx.compose.runtime.*
@@ -575,7 +574,7 @@
 
     @Test
     @Ignore("b/173733968")
-    fun testSimpleClassResolution(): Unit = ensureSetup {
+    fun testSimpleClassResolution() {
         compose(
             """
             import androidx.compose.runtime.*
@@ -592,7 +591,7 @@
 
     @Test
     @Ignore("b/173733968")
-    fun testSetContent(): Unit = ensureSetup {
+    fun testSetContent() {
         codegen(
             """
                 fun fakeCompose(block: @Composable ()->Unit) { }
@@ -610,7 +609,7 @@
 
     @Test
     @Ignore("b/173733968")
-    fun testComposeWithResult(): Unit = ensureSetup {
+    fun testComposeWithResult() {
         compose(
             """
                 @Composable fun <T> identity(block: @Composable ()->T): T = block()
@@ -630,7 +629,7 @@
 
     @Test
     @Ignore("b/173733968")
-    fun testObservable(): Unit = ensureSetup {
+    fun testObservable() {
         compose(
             """
                 import androidx.compose.runtime.*
@@ -659,7 +658,7 @@
 
     @Test
     @Ignore("b/173733968")
-    fun testObservableLambda(): Unit = ensureSetup {
+    fun testObservableLambda() {
         compose(
             """
                 @Composable
@@ -692,7 +691,7 @@
 
     @Test
     @Ignore("b/173733968")
-    fun testObservableGenericFunction(): Unit = ensureSetup {
+    fun testObservableGenericFunction() {
         compose(
             """
             @Composable
@@ -718,7 +717,7 @@
 
     @Test
     @Ignore("b/173733968")
-    fun testObservableExtension(): Unit = ensureSetup {
+    fun testObservableExtension() {
         compose(
             """
             @Composable
@@ -746,7 +745,7 @@
 
     @Test
     @Ignore("b/173733968")
-    fun testObserverableExpressionBody(): Unit = ensureSetup {
+    fun testObserverableExpressionBody() {
         compose(
             """
             @Composable
@@ -776,7 +775,7 @@
 
     @Test
     @Ignore("b/173733968")
-    fun testObservableInlineWrapper(): Unit = ensureSetup {
+    fun testObservableInlineWrapper() {
         compose(
             """
             var inWrapper = false
@@ -816,7 +815,7 @@
 
     @Test
     @Ignore("b/173733968")
-    fun testObservableDefaultParameter(): Unit = ensureSetup {
+    fun testObservableDefaultParameter() {
         compose(
             """
             val counter = mutableStateOf(0)
@@ -844,7 +843,7 @@
 
     @Test
     @Ignore("b/173733968")
-    fun testObservableEarlyReturn(): Unit = ensureSetup {
+    fun testObservableEarlyReturn() {
         compose(
             """
             val counter = mutableStateOf(0)
@@ -884,7 +883,7 @@
 
     @Test
     @Ignore("b/173733968")
-    fun testCGSimpleTextView(): Unit = ensureSetup {
+    fun testCGSimpleTextView() {
         compose(
             """
 
@@ -900,7 +899,7 @@
 
     @Test
     @Ignore("b/173733968")
-    fun testCGLocallyScopedFunction(): Unit = ensureSetup {
+    fun testCGLocallyScopedFunction() {
         compose(
             """
                 @Composable
@@ -922,7 +921,7 @@
 
     @Test
     @Ignore("b/173733968")
-    fun testCGLocallyScopedExtensionFunction(): Unit = ensureSetup {
+    fun testCGLocallyScopedExtensionFunction() {
         compose(
             """
                 @Composable
@@ -944,7 +943,7 @@
 
     @Test
     @Ignore("b/173733968")
-    fun testImplicitReceiverScopeCall(): Unit = ensureSetup {
+    fun testImplicitReceiverScopeCall() {
         compose(
             """
                 import androidx.compose.runtime.*
@@ -973,7 +972,7 @@
 
     @Test
     @Ignore("b/173733968")
-    fun testCGLocallyScopedInvokeOperator(): Unit = ensureSetup {
+    fun testCGLocallyScopedInvokeOperator() {
         compose(
             """
                 @Composable
@@ -996,7 +995,7 @@
 
     @Test
     @Ignore("b/173733968")
-    fun testTrivialExtensionFunction(): Unit = ensureSetup {
+    fun testTrivialExtensionFunction() {
         compose(
             """ """,
             """
@@ -1009,7 +1008,7 @@
 
     @Test
     @Ignore("b/173733968")
-    fun testTrivialInvokeExtensionFunction(): Unit = ensureSetup {
+    fun testTrivialInvokeExtensionFunction() {
         compose(
             """ """,
             """
@@ -1022,7 +1021,7 @@
 
     @Test
     @Ignore("b/173733968")
-    fun testCGNSimpleTextView(): Unit = ensureSetup {
+    fun testCGNSimpleTextView() {
         compose(
             """
 
@@ -1038,7 +1037,7 @@
 
     @Test
     @Ignore("b/173733968")
-    fun testInliningTemp2(): Unit = ensureSetup {
+    fun testInliningTemp2() {
         compose(
             """
                 @Composable
@@ -1055,7 +1054,7 @@
 
     @Test
     @Ignore("b/173733968")
-    fun testInliningTemp3(): Unit = ensureSetup {
+    fun testInliningTemp3() {
         compose(
             """
                 @Composable
@@ -1072,7 +1071,7 @@
 
     @Test
     @Ignore("b/173733968")
-    fun testInliningTemp4(): Unit = ensureSetup {
+    fun testInliningTemp4() {
         compose(
             """
                 @Composable
@@ -1089,7 +1088,7 @@
 
     @Test
     @Ignore("b/173733968")
-    fun testInline_NonComposable_Identity(): Unit = ensureSetup {
+    fun testInline_NonComposable_Identity() {
         compose(
             """
             @Composable inline fun InlineWrapper(base: Int, content: @Composable ()->Unit) {
@@ -1108,7 +1107,7 @@
 
     @Test
     @Ignore("b/173733968")
-    fun testInline_Composable_Identity(): Unit = ensureSetup {
+    fun testInline_Composable_Identity() {
         compose(
             """
             """,
@@ -1122,7 +1121,7 @@
 
     @Test
     @Ignore("b/173733968")
-    fun testInline_Composable_EmitChildren(): Unit = ensureSetup {
+    fun testInline_Composable_EmitChildren() {
         compose(
             """
             @Composable
@@ -1148,7 +1147,7 @@
 
     @Test
     @Ignore("b/173733968")
-    fun testCGNInlining(): Unit = ensureSetup {
+    fun testCGNInlining() {
         compose(
             """
 
@@ -1166,7 +1165,7 @@
 
     @Test
     @Ignore("b/173733968")
-    fun testInlineClassesAsComposableParameters(): Unit = ensureSetup {
+    fun testInlineClassesAsComposableParameters() {
         codegen(
             """
                 inline class WrappedInt(val int: Int)
@@ -1184,7 +1183,7 @@
 
     @Test
     @Ignore("b/173733968")
-    fun testInlineClassesAsDefaultParameters(): Unit = ensureSetup {
+    fun testInlineClassesAsDefaultParameters() {
         compose(
             """
                 inline class Positive(val int: Int) {
@@ -1204,7 +1203,7 @@
 
     @Test
     @Ignore("b/173733968")
-    fun testRangeForLoop(): Unit = ensureSetup {
+    fun testRangeForLoop() {
         codegen(
             """
                 @Composable fun Foo(i: Int) {}
@@ -1220,7 +1219,7 @@
 
     @Test
     @Ignore("b/173733968")
-    fun testReturnValue(): Unit = ensureSetup {
+    fun testReturnValue() {
         compose(
             """
             var a = 0
@@ -1287,7 +1286,7 @@
 
     @Test
     @Ignore("b/173733968")
-    fun testReorderedArgsReturnValue(): Unit = ensureSetup {
+    fun testReorderedArgsReturnValue() {
         compose(
             """
             @Composable
@@ -1309,7 +1308,7 @@
 
     @Test
     @Ignore("b/173733968")
-    fun testTrivialReturnValue(): Unit = ensureSetup {
+    fun testTrivialReturnValue() {
         compose(
             """
         @Composable
@@ -1334,7 +1333,7 @@
 
     @Test
     @Ignore("b/173733968")
-    fun testForDevelopment(): Unit = ensureSetup {
+    fun testForDevelopment() {
         codegen(
             """
             import androidx.compose.runtime.*
@@ -1354,7 +1353,7 @@
 
     @Test
     @Ignore("b/173733968")
-    fun testInliningTemp(): Unit = ensureSetup {
+    fun testInliningTemp() {
         compose(
             """
                 @Composable
@@ -1376,7 +1375,7 @@
 
     @Test
     @Ignore("b/173733968")
-    fun testCGUpdatedComposition(): Unit = ensureSetup {
+    fun testCGUpdatedComposition() {
         var value = "Hello, world!"
 
         compose(
@@ -1398,7 +1397,7 @@
 
     @Test
     @Ignore("b/173733968")
-    fun testCGNUpdatedComposition(): Unit = ensureSetup {
+    fun testCGNUpdatedComposition() {
         var value = "Hello, world!"
 
         compose(
@@ -1420,7 +1419,7 @@
 
     @Test
     @Ignore("b/173733968")
-    fun testCGViewGroup(): Unit = ensureSetup {
+    fun testCGViewGroup() {
         val tvId = 258
         val llId = 260
         var text = "Hello, world!"
@@ -1454,7 +1453,7 @@
 
     @Test
     @Ignore("b/173733968")
-    fun testCGNFunctionComponent(): Unit = ensureSetup {
+    fun testCGNFunctionComponent() {
         var text = "Hello, world!"
         val tvId = 123
 
@@ -1484,7 +1483,7 @@
 
     @Test
     @Ignore("b/173733968")
-    fun testCompositionLocalConsumedFromDefaultParameter(): Unit = ensureSetup {
+    fun testCompositionLocalConsumedFromDefaultParameter() {
         val initialText = "no text"
         val helloWorld = "Hello World!"
         compose(
@@ -1528,7 +1527,7 @@
 
     @Test
     @Ignore("b/173733968")
-    fun testCGNViewGroup(): Unit = ensureSetup {
+    fun testCGNViewGroup() {
         val tvId = 258
         val llId = 260
         var text = "Hello, world!"
@@ -1562,7 +1561,7 @@
 
     @Test
     @Ignore("b/173733968")
-    fun testMemoization(): Unit = ensureSetup {
+    fun testMemoization() {
         val tvId = 258
         val tagId = (3 shl 24) or "composed_set".hashCode()
 
@@ -1642,7 +1641,7 @@
 
     @Test
     @Ignore("b/173733968")
-    fun testInlineClassMemoization(): Unit = ensureSetup {
+    fun testInlineClassMemoization() {
         val tvId = 258
         val tagId = (3 shl 24) or "composed_set".hashCode()
 
@@ -1729,7 +1728,7 @@
 
     @Test
     @Ignore("b/173733968")
-    fun testStringParameterMemoization(): Unit = ensureSetup {
+    fun testStringParameterMemoization() {
         val tvId = 258
         val tagId = (3 shl 24) or "composed_set".hashCode()
 
@@ -1777,7 +1776,7 @@
 
     @Test
     @Ignore("b/173733968")
-    fun testCGNSimpleCall(): Unit = ensureSetup {
+    fun testCGNSimpleCall() {
         val tvId = 258
         var text = "Hello, world!"
 
@@ -1806,7 +1805,7 @@
 
     @Test
     @Ignore("b/173733968")
-    fun testCGNCallWithChildren(): Unit = ensureSetup {
+    fun testCGNCallWithChildren() {
         val tvId = 258
         var text = "Hello, world!"
 
@@ -1840,7 +1839,7 @@
 
     @Test
     @Ignore("b/173733968")
-    fun testCGComposableFunctionInvocationOneParameter(): Unit = ensureSetup {
+    fun testCGComposableFunctionInvocationOneParameter() {
         val tvId = 91
         var phone = "(123) 456-7890"
         compose(
@@ -1867,7 +1866,7 @@
 
     @Test
     @Ignore("b/173733968")
-    fun testCGComposableFunctionInvocationTwoParameters(): Unit = ensureSetup {
+    fun testCGComposableFunctionInvocationTwoParameters() {
         val tvId = 111
         val rsId = 112
         var left = 0
@@ -1927,7 +1926,7 @@
 
     @Test
     @Ignore("b/173733968")
-    fun testImplicitReceiverPassing1(): Unit = ensureSetup {
+    fun testImplicitReceiverPassing1() {
         compose(
             """
                 @Composable fun Int.Foo(x: @Composable Int.() -> Unit) {
@@ -1950,7 +1949,7 @@
 
     @Test
     @Ignore("b/173733968")
-    fun testImplicitReceiverPassing2(): Unit = ensureSetup {
+    fun testImplicitReceiverPassing2() {
         compose(
             """
                 @Composable fun Int.Foo(x: @Composable Int.(text: String) -> Unit, text: String) {
@@ -1977,7 +1976,7 @@
 
     @Test
     @Ignore("b/173733968")
-    fun testEffects1(): Unit = ensureSetup {
+    fun testEffects1() {
         compose(
             """
                 @Composable
@@ -2008,7 +2007,7 @@
 
     @Test
     @Ignore("b/173733968")
-    fun testEffects2(): Unit = ensureSetup {
+    fun testEffects2() {
         compose(
             """
                 @Composable
@@ -2039,7 +2038,7 @@
 
     @Ignore("b/171801506")
     @Test
-    fun testEffects3(): Unit = ensureSetup {
+    fun testEffects3() {
         val log = StringBuilder()
         compose(
             """
@@ -2081,7 +2080,7 @@
 
     @Ignore("b/171801506")
     @Test
-    fun testEffects4(): Unit = ensureSetup {
+    fun testEffects4() {
         val log = StringBuilder()
         compose(
             """
@@ -2125,7 +2124,7 @@
 
     @Test
     @Ignore("b/173733968")
-    fun testVariableCalls1(): Unit = ensureSetup {
+    fun testVariableCalls1() {
         compose(
             """
                 val component = @Composable {
@@ -2144,7 +2143,7 @@
 
     @Test
     @Ignore("b/173733968")
-    fun testVariableCalls2(): Unit = ensureSetup {
+    fun testVariableCalls2() {
         compose(
             """
                 val component = @Composable {
@@ -2167,7 +2166,7 @@
 
     @Test
     @Ignore("b/173733968")
-    fun testVariableCalls3(): Unit = ensureSetup {
+    fun testVariableCalls3() {
         compose(
             """
                 val component = @Composable {
@@ -2196,7 +2195,7 @@
     // b/123721921
     @Test
     @Ignore("b/173733968")
-    fun testDefaultParameters1(): Unit = ensureSetup {
+    fun testDefaultParameters1() {
         compose(
             """
                 @Composable
@@ -2216,7 +2215,7 @@
 
     @Test
     @Ignore("b/173733968")
-    fun testDefaultParameters2(): Unit = ensureSetup {
+    fun testDefaultParameters2() {
         compose(
             """
                 @Composable
@@ -2237,7 +2236,7 @@
 
     @Test
     @Ignore("b/173733968")
-    fun testMovement(): Unit = ensureSetup {
+    fun testMovement() {
         val tvId = 50
         val btnIdAdd = 100
         val btnIdUp = 200
@@ -2324,7 +2323,7 @@
 
     @Test
     @Ignore("b/173733968")
-    fun testObserveKtxWithInline(): Unit = ensureSetup {
+    fun testObserveKtxWithInline() {
         compose(
             """
                 @Composable
@@ -2362,7 +2361,7 @@
 
     @Test
     @Ignore("b/173733968")
-    fun testKeyTag(): Unit = ensureSetup {
+    fun testKeyTag() {
         compose(
             """
             val list = mutableStateListOf(0,1,2,3)
@@ -2412,7 +2411,7 @@
 
     @Test
     @Ignore("b/173733968")
-    fun testNonComposeParameters(): Unit = ensureSetup {
+    fun testNonComposeParameters() {
         compose(
             """
                 class Action(
@@ -2434,7 +2433,7 @@
 
     @Ignore("b/171801506")
     @Test
-    fun testStableParameters_Various(): Unit = ensureSetup {
+    fun testStableParameters_Various() {
         val output = ArrayList<String>()
         compose(
             """
@@ -2594,7 +2593,7 @@
 
     @Test
     @Ignore("b/173733968")
-    fun testStableParameters_Lambdas(): Unit = ensureSetup {
+    fun testStableParameters_Lambdas() {
         val output = ArrayList<String>()
         compose(
             """
@@ -2675,7 +2674,7 @@
 
     @Test
     @Ignore("b/173733968")
-    fun testRecomposeScope(): Unit = ensureSetup {
+    fun testRecomposeScope() {
         compose(
             """
             val m = mutableStateOf(0)
@@ -2728,7 +2727,7 @@
 
     @Test
     @Ignore("b/173733968")
-    fun testRecomposeScope_ReceiverScope(): Unit = ensureSetup {
+    fun testRecomposeScope_ReceiverScope() {
         compose(
             """
             val m = mutableStateOf(0)
@@ -2763,7 +2762,7 @@
 
     @Test
     @Ignore("b/173733968")
-    fun testCompose_InlineReceiver(): Unit = ensureSetup {
+    fun testCompose_InlineReceiver() {
         compose(
             """
             object Context {
@@ -2785,7 +2784,7 @@
 
     @Test
     @Ignore("b/173733968")
-    fun testRecomposeScope_Method(): Unit = ensureSetup {
+    fun testRecomposeScope_Method() {
         compose(
             """
             val m = mutableStateOf(0)
@@ -2832,9 +2831,3 @@
 fun View.getComposedSet(tagId: Int): Set<String>? = getTag(tagId) as? Set<String>
 
 private val noParameters = { emptyMap<String, String>() }
-
-private inline fun <reified T : PsiElement> PsiElement.parentOfType(): T? = parentOfType(T::class)
-
-private fun <T : PsiElement> PsiElement.parentOfType(vararg classes: KClass<out T>): T? {
-    return PsiTreeUtil.getParentOfType(this, *classes.map { it.java }.toTypedArray())
-}
\ No newline at end of file
diff --git a/compose/compiler/compiler-hosted/integration-tests/src/test/java/androidx/compose/compiler/plugins/kotlin/ComposeCallResolverTests.kt b/compose/compiler/compiler-hosted/integration-tests/src/test/java/androidx/compose/compiler/plugins/kotlin/ComposeCallResolverTests.kt
index 9b45bd8..eee207e 100644
--- a/compose/compiler/compiler-hosted/integration-tests/src/test/java/androidx/compose/compiler/plugins/kotlin/ComposeCallResolverTests.kt
+++ b/compose/compiler/compiler-hosted/integration-tests/src/test/java/androidx/compose/compiler/plugins/kotlin/ComposeCallResolverTests.kt
@@ -16,20 +16,18 @@
 
 package androidx.compose.compiler.plugins.kotlin
 
+import androidx.compose.compiler.plugins.kotlin.facade.SourceFile
 import com.intellij.psi.PsiElement
-import com.intellij.psi.util.PsiTreeUtil
 import org.jetbrains.kotlin.psi.KtBlockExpression
 import org.jetbrains.kotlin.psi.KtDeclaration
 import org.jetbrains.kotlin.psi.KtElement
-import org.jetbrains.kotlin.psi.KtFile
-import org.jetbrains.kotlin.psi.KtPsiFactory
 import org.jetbrains.kotlin.resolve.BindingContext
-import org.jetbrains.kotlin.resolve.calls.util.getResolvedCall
 import org.jetbrains.kotlin.resolve.calls.model.ResolvedCall
-import kotlin.reflect.KClass
+import org.jetbrains.kotlin.resolve.calls.util.getResolvedCall
+import org.junit.Test
 
 class ComposeCallResolverTests : AbstractCodegenTest() {
-
+    @Test
     fun testProperties() = assertInterceptions(
         """
             import androidx.compose.runtime.*
@@ -52,6 +50,7 @@
         """
     )
 
+    @Test
     fun testBasicCallTypes() = assertInterceptions(
         """
             import androidx.compose.runtime.*
@@ -69,6 +68,7 @@
         """
     )
 
+    @Test
     fun testReceiverScopeCall() = assertInterceptions(
         """
             import androidx.compose.runtime.*
@@ -87,6 +87,7 @@
         """
     )
 
+    @Test
     fun testInvokeOperatorCall() = assertInterceptions(
         """
             import androidx.compose.runtime.*
@@ -101,6 +102,7 @@
         """
     )
 
+    @Test
     fun testComposableLambdaCall() = assertInterceptions(
         """
             import androidx.compose.runtime.*
@@ -112,6 +114,7 @@
         """
     )
 
+    @Test
     fun testComposableLambdaCallWithGenerics() = assertInterceptions(
         """
             import androidx.compose.runtime.*
@@ -155,6 +158,7 @@
         """
     )
 
+    @Test
     fun testReceiverLambdaInvocation() = assertInterceptions(
         """
             import androidx.compose.runtime.*
@@ -172,6 +176,7 @@
         """
     )
 
+    @Test
     fun testReceiverLambda2() = assertInterceptions(
         """
             import androidx.compose.runtime.*
@@ -192,6 +197,7 @@
         """
     )
 
+    @Test
     fun testInlineContent() = assertInterceptions(
         """
             import androidx.compose.runtime.*
@@ -211,23 +217,15 @@
         """
     )
 
-    private fun <T> setup(block: () -> T): T {
-        return block()
-    }
-
-    fun assertInterceptions(srcText: String) = setup {
+    private fun assertInterceptions(srcText: String) {
         val (text, carets) = extractCarets(srcText)
 
-        val environment = myEnvironment ?: error("Environment not initialized")
-
-        val ktFile = KtPsiFactory(environment.project).createFile(text)
-        val bindingContext = JvmResolveUtil.analyzeAndCheckForErrors(
-            environment,
-            listOf(ktFile)
-        ).bindingContext
+        val analysisResult = analyze(listOf(SourceFile("test.kt", text)))
+        val bindingContext = analysisResult.bindingContext!!
+        val ktFile = analysisResult.files.single()
 
         carets.forEachIndexed { index, (offset, calltype) ->
-            val resolvedCall = resolvedCallAtOffset(bindingContext, ktFile, offset)
+            val resolvedCall = ktFile.findElementAt(offset)?.getNearestResolvedCall(bindingContext)
                 ?: error(
                     "No resolved call found at index: $index, offset: $offset. Expected " +
                         "$calltype."
@@ -241,7 +239,7 @@
         }
     }
 
-    private val callPattern = Regex("(<normal>)|(<emit>)|(<call>)")
+    private val callPattern = Regex("(<normal>)|(<call>)")
     private fun extractCarets(text: String): Pair<String, List<Pair<Int, String>>> {
         val indices = mutableListOf<Pair<Int, String>>()
         var offset = 0
@@ -252,15 +250,6 @@
         }
         return src to indices
     }
-
-    private fun resolvedCallAtOffset(
-        bindingContext: BindingContext,
-        jetFile: KtFile,
-        index: Int
-    ): ResolvedCall<*>? {
-        val element = jetFile.findElementAt(index)!!
-        return element.getNearestResolvedCall(bindingContext)
-    }
 }
 
 fun PsiElement?.getNearestResolvedCall(bindingContext: BindingContext): ResolvedCall<*>? {
@@ -280,9 +269,3 @@
     }
     return null
 }
-
-private inline fun <reified T : PsiElement> PsiElement.parentOfType(): T? = parentOfType(T::class)
-
-private fun <T : PsiElement> PsiElement.parentOfType(vararg classes: KClass<out T>): T? {
-    return PsiTreeUtil.getParentOfType(this, *classes.map { it.java }.toTypedArray())
-}
\ No newline at end of file
diff --git a/compose/compiler/compiler-hosted/integration-tests/src/test/java/androidx/compose/compiler/plugins/kotlin/ComposeMultiPlatformTests.kt b/compose/compiler/compiler-hosted/integration-tests/src/test/java/androidx/compose/compiler/plugins/kotlin/ComposeMultiPlatformTests.kt
index b5b7db5..6effc56 100644
--- a/compose/compiler/compiler-hosted/integration-tests/src/test/java/androidx/compose/compiler/plugins/kotlin/ComposeMultiPlatformTests.kt
+++ b/compose/compiler/compiler-hosted/integration-tests/src/test/java/androidx/compose/compiler/plugins/kotlin/ComposeMultiPlatformTests.kt
@@ -20,9 +20,8 @@
 
 @Suppress("unused")
 class ComposeMultiPlatformTests : AbstractMultiPlatformIntegrationTest() {
-
     @Test
-    fun testBasicMpp() = ensureSetup {
+    fun testBasicMpp() {
         multiplatform(
             """
             expect val foo: String
@@ -41,7 +40,7 @@
     }
 
     @Test
-    fun testBasicComposable() = ensureSetup {
+    fun testBasicComposable() {
         multiplatform(
             """
             import androidx.compose.runtime.Composable
@@ -71,7 +70,7 @@
     }
 
     @Test
-    fun testComposableExpectDefaultParameter() = ensureSetup {
+    fun testComposableExpectDefaultParameter() {
         multiplatform(
             """
                 import androidx.compose.runtime.Composable
diff --git a/compose/compiler/compiler-hosted/integration-tests/src/test/java/androidx/compose/compiler/plugins/kotlin/ComposerParamSignatureTests.kt b/compose/compiler/compiler-hosted/integration-tests/src/test/java/androidx/compose/compiler/plugins/kotlin/ComposerParamSignatureTests.kt
index e0d3e00..a7e8e11 100644
--- a/compose/compiler/compiler-hosted/integration-tests/src/test/java/androidx/compose/compiler/plugins/kotlin/ComposerParamSignatureTests.kt
+++ b/compose/compiler/compiler-hosted/integration-tests/src/test/java/androidx/compose/compiler/plugins/kotlin/ComposerParamSignatureTests.kt
@@ -16,6 +16,8 @@
 
 package androidx.compose.compiler.plugins.kotlin
 
+import org.junit.Assert.assertFalse
+import org.junit.Assert.assertTrue
 import org.junit.Ignore
 import org.junit.Test
 import org.junit.runner.RunWith
@@ -30,9 +32,8 @@
     maxSdk = 23
 )
 class ComposerParamSignatureTests : AbstractCodegenSignatureTest() {
-
     @Test
-    fun testParameterlessChildrenLambdasReused(): Unit = checkApi(
+    fun testParameterlessChildrenLambdasReused() = checkApi(
         """
             @Composable fun Foo(content: @Composable () -> Unit) {
             }
@@ -89,7 +90,7 @@
     )
 
     @Test
-    fun testNoComposerNullCheck(): Unit = validateBytecode(
+    fun testNoComposerNullCheck() = validateBytecode(
         """
         @Composable fun Foo() {}
         """
@@ -98,7 +99,7 @@
     }
 
     @Test
-    fun testStrangeReceiverIssue(): Unit = codegen(
+    fun testStrangeReceiverIssue() = codegen(
         """
         import androidx.compose.runtime.ExplicitGroupsComposable
         import androidx.compose.runtime.NonRestartableComposable
@@ -125,7 +126,7 @@
     )
 
     @Test
-    fun testArrayListSizeOverride(): Unit = validateBytecode(
+    fun testArrayListSizeOverride() = validateBytecode(
         """
         class CustomList : ArrayList<Any>() {
             override val size: Int
@@ -138,7 +139,7 @@
     }
 
     @Test
-    fun testForLoopIssue1(): Unit = codegen(
+    fun testForLoopIssue1() = codegen(
         """
             @Composable
             fun Test(text: String, callback: @Composable () -> Unit) {
@@ -153,7 +154,7 @@
     )
 
     @Test
-    fun testForLoopIssue2(): Unit = codegen(
+    fun testForLoopIssue2() = codegen(
         """
             @Composable
             fun Test(text: List<String>, callback: @Composable () -> Unit) {
@@ -168,7 +169,7 @@
     )
 
     @Test
-    fun testCaptureIssue23(): Unit = codegen(
+    fun testCaptureIssue23() = codegen(
         """
             import androidx.compose.animation.AnimatedContent
             import androidx.compose.animation.ExperimentalAnimationApi
@@ -187,7 +188,7 @@
     )
 
     @Test
-    fun test32Params(): Unit = codegen(
+    fun test32Params() = codegen(
         """
         @Composable
         fun <T> TooVerbose(
@@ -212,7 +213,7 @@
     )
 
     @Test
-    fun testInterfaceMethodWithComposableParameter(): Unit = validateBytecode(
+    fun testInterfaceMethodWithComposableParameter() = validateBytecode(
         """
             @Composable
             fun test1(cc: ControlledComposition) {
@@ -227,7 +228,7 @@
     }
 
     @Test
-    fun testFakeOverrideFromSameModuleButLaterTraversal(): Unit = validateBytecode(
+    fun testFakeOverrideFromSameModuleButLaterTraversal() = validateBytecode(
         """
             class B : A() {
                 fun test() {
@@ -243,7 +244,7 @@
     }
 
     @Test
-    fun testPrimitiveChangedCalls(): Unit = validateBytecode(
+    fun testPrimitiveChangedCalls() = validateBytecode(
         """
         @Composable fun Foo(
             a: Boolean,
@@ -278,7 +279,7 @@
     }
 
     @Test
-    fun testNonPrimitiveChangedCalls(): Unit = validateBytecode(
+    fun testNonPrimitiveChangedCalls() = validateBytecode(
         """
         import androidx.compose.runtime.Stable
 
@@ -300,7 +301,7 @@
     }
 
     @Test
-    fun testInlineClassChangedCalls(): Unit = validateBytecode(
+    fun testInlineClassChangedCalls() = validateBytecode(
         """
         inline class Bar(val value: Int)
         @Composable fun Foo(a: Bar) {
@@ -316,7 +317,7 @@
     }
 
     @Test
-    fun testNullableInlineClassChangedCalls(): Unit = validateBytecode(
+    fun testNullableInlineClassChangedCalls() = validateBytecode(
         """
         inline class Bar(val value: Int)
         @Composable fun Foo(a: Bar?) {
@@ -343,7 +344,7 @@
     }
 
     @Test
-    fun testNoNullCheckForPassedParameters(): Unit = validateBytecode(
+    fun testNoNullCheckForPassedParameters() = validateBytecode(
         """
         inline class Bar(val value: Int)
         fun nonNull(bar: Bar) {}
@@ -356,7 +357,7 @@
     }
 
     @Test
-    fun testNoComposerNullCheck2(): Unit = validateBytecode(
+    fun testNoComposerNullCheck2() = validateBytecode(
         """
         val foo = @Composable {}
         val bar = @Composable { x: Int -> }
@@ -366,7 +367,7 @@
     }
 
     @Test
-    fun testComposableLambdaInvoke(): Unit = validateBytecode(
+    fun testComposableLambdaInvoke() = validateBytecode(
         """
         @Composable fun NonNull(content: @Composable() () -> Unit) {
             content.invoke()
@@ -384,7 +385,7 @@
     }
 
     @Test
-    fun testAnonymousParamNaming(): Unit = validateBytecode(
+    fun testAnonymousParamNaming() = validateBytecode(
         """
         @Composable
         fun Foo(content: @Composable (a: Int, b: Int) -> Unit) {}
@@ -398,7 +399,7 @@
     }
 
     @Test
-    fun testBasicClassStaticTransform(): Unit = checkApi(
+    fun testBasicClassStaticTransform() = checkApi(
         """
             class Foo
         """,
@@ -412,7 +413,7 @@
     )
 
     @Test
-    fun testLambdaReorderedParameter(): Unit = checkApi(
+    fun testLambdaReorderedParameter() = checkApi(
         """
             @Composable fun Foo(a: String, b: () -> Unit) { }
             @Composable fun Example() {
@@ -458,7 +459,7 @@
     )
 
     @Test
-    fun testCompositionLocalCurrent(): Unit = checkApi(
+    fun testCompositionLocalCurrent() = checkApi(
         """
             val a = compositionLocalOf { 123 }
             @Composable fun Foo() {
@@ -496,7 +497,7 @@
     )
 
     @Test
-    fun testRemappedTypes(): Unit = checkApi(
+    fun testRemappedTypes() = checkApi(
         """
             class A {
                 fun makeA(): A { return A() }
@@ -539,7 +540,7 @@
     )
 
     @Test
-    fun testDataClassHashCode(): Unit = validateBytecode(
+    fun testDataClassHashCode() = validateBytecode(
         """
         data class Foo(
             val bar: @Composable () -> Unit
@@ -551,7 +552,7 @@
 
     @Test
     @Ignore("b/179279455")
-    fun testCorrectComposerPassed1(): Unit = checkComposerParam(
+    fun testCorrectComposerPassed1() = checkComposerParam(
         """
             var a: Composer? = null
             fun run() {
@@ -565,7 +566,7 @@
 
     @Test
     @Ignore("b/179279455")
-    fun testCorrectComposerPassed2(): Unit = checkComposerParam(
+    fun testCorrectComposerPassed2() = checkComposerParam(
         """
             var a: Composer? = null
             @Composable fun Foo() {
@@ -582,7 +583,7 @@
 
     @Test
     @Ignore("b/179279455")
-    fun testCorrectComposerPassed3(): Unit = checkComposerParam(
+    fun testCorrectComposerPassed3() = checkComposerParam(
         """
             var a: Composer? = null
             var b: Composer? = null
@@ -606,7 +607,7 @@
 
     @Test
     @Ignore("b/179279455")
-    fun testCorrectComposerPassed4(): Unit = checkComposerParam(
+    fun testCorrectComposerPassed4() = checkComposerParam(
         """
             var a: Composer? = null
             var b: Composer? = null
@@ -633,7 +634,7 @@
 
     @Test
     @Ignore("b/179279455")
-    fun testCorrectComposerPassed5(): Unit = checkComposerParam(
+    fun testCorrectComposerPassed5() = checkComposerParam(
         """
             var a: Composer? = null
             @Composable fun Wrap(content: @Composable () -> Unit) {
@@ -658,7 +659,7 @@
     )
 
     @Test
-    fun testDefaultParameters(): Unit = checkApi(
+    fun testDefaultParameters() = checkApi(
         """
             @Composable fun Foo(x: Int = 0) {
 
@@ -683,7 +684,7 @@
     )
 
     @Test
-    fun testDefaultExpressionsWithComposableCall(): Unit = checkApi(
+    fun testDefaultExpressionsWithComposableCall() = checkApi(
         """
             @Composable fun <T> identity(value: T): T = value
             @Composable fun Foo(x: Int = identity(20)) {
@@ -724,7 +725,7 @@
     )
 
     @Test
-    fun testBasicCallAndParameterUsage(): Unit = checkApi(
+    fun testBasicCallAndParameterUsage() = checkApi(
         """
             @Composable fun Foo(a: Int, b: String) {
                 print(a)
@@ -768,7 +769,7 @@
     )
 
     @Test
-    fun testCallFromInlinedLambda(): Unit = checkApi(
+    fun testCallFromInlinedLambda() = checkApi(
         """
             @Composable fun Foo() {
                 listOf(1, 2, 3).forEach { Bar(it) }
@@ -804,7 +805,7 @@
     )
 
     @Test
-    fun testBasicLambda(): Unit = checkApi(
+    fun testBasicLambda() = checkApi(
         """
             val foo = @Composable { x: Int -> print(x)  }
             @Composable fun Bar() {
@@ -848,7 +849,7 @@
     )
 
     @Test
-    fun testLocalLambda(): Unit = checkApi(
+    fun testLocalLambda() = checkApi(
         """
             @Composable fun Bar(content: @Composable () -> Unit) {
                 val foo = @Composable { x: Int -> print(x)  }
@@ -891,7 +892,7 @@
     )
 
     @Test
-    fun testNesting(): Unit = checkApi(
+    fun testNesting() = checkApi(
         """
             @Composable fun Wrap(content: @Composable (x: Int) -> Unit) {
                 content(123)
@@ -958,7 +959,7 @@
     )
 
     @Test
-    fun testComposableInterface(): Unit = checkApi(
+    fun testComposableInterface() = checkApi(
         """
             interface Foo {
                 @Composable fun bar()
@@ -992,7 +993,7 @@
     )
 
     @Test
-    fun testSealedClassEtc(): Unit = checkApi(
+    fun testSealedClassEtc() = checkApi(
         """
             sealed class CompositionLocal2<T> {
                 inline val current: T
@@ -1043,7 +1044,7 @@
     )
 
     @Test
-    fun testComposableTopLevelProperty(): Unit = checkApi(
+    fun testComposableTopLevelProperty() = checkApi(
         """
             val foo: Int @Composable get() { return 123 }
         """,
@@ -1055,7 +1056,7 @@
     )
 
     @Test
-    fun testComposableProperty(): Unit = checkApi(
+    fun testComposableProperty() = checkApi(
         """
             class Foo {
                 val foo: Int @Composable get() { return 123 }
@@ -1072,7 +1073,7 @@
     )
 
     @Test
-    fun testTableLambdaThing(): Unit = validateBytecode(
+    fun testTableLambdaThing() = validateBytecode(
         """
         @Composable
         fun Foo() {
@@ -1087,7 +1088,7 @@
     }
 
     @Test
-    fun testDefaultArgs(): Unit = validateBytecode(
+    fun testDefaultArgs() = validateBytecode(
         """
         @Composable
         fun Scaffold(
@@ -1099,7 +1100,7 @@
     }
 
     @Test
-    fun testSyntheticAccessFunctions(): Unit = validateBytecode(
+    fun testSyntheticAccessFunctions() = validateBytecode(
         """
         class Foo {
             @Composable private fun Bar() {}
@@ -1110,7 +1111,7 @@
     }
 
     @Test
-    fun testLambdaMemoization(): Unit = validateBytecode(
+    fun testLambdaMemoization() = validateBytecode(
         """
         fun subcompose(block: @Composable () -> Unit) {}
         private class Foo {
@@ -1128,7 +1129,7 @@
     }
 
     @Test
-    fun testCallingProperties(): Unit = checkApi(
+    fun testCallingProperties() = checkApi(
         """
             val bar: Int @Composable get() { return 123 }
 
@@ -1154,7 +1155,7 @@
     )
 
     @Test
-    fun testAbstractComposable(): Unit = checkApi(
+    fun testAbstractComposable() = checkApi(
         """
             abstract class BaseFoo {
                 @Composable abstract fun bar()
@@ -1191,7 +1192,7 @@
     )
 
     @Test
-    fun testLocalClassAndObjectLiterals(): Unit = checkApi(
+    fun testLocalClassAndObjectLiterals() = checkApi(
         """
             @Composable
             fun Wat() {}
@@ -1243,7 +1244,7 @@
     )
 
     @Test
-    fun testNonComposableCode(): Unit = checkApi(
+    fun testNonComposableCode() = checkApi(
         """
             fun A() {}
             val b: Int get() = 123
@@ -1314,7 +1315,7 @@
     )
 
     @Test
-    fun testCircularCall(): Unit = checkApi(
+    fun testCircularCall() = checkApi(
         """
             @Composable fun Example() {
                 Example()
@@ -1337,7 +1338,7 @@
     )
 
     @Test
-    fun testInlineCall(): Unit = checkApi(
+    fun testInlineCall() = checkApi(
         """
             @Composable inline fun Example(content: @Composable () -> Unit) {
                 content()
@@ -1365,7 +1366,7 @@
     )
 
     @Test
-    fun testDexNaming(): Unit = checkApi(
+    fun testDexNaming() = checkApi(
         """
             val myProperty: () -> Unit @Composable get() {
                 return {  }
@@ -1389,7 +1390,7 @@
     )
 
     @Test
-    fun testInnerClass(): Unit = checkApi(
+    fun testInnerClass() = checkApi(
         """
             interface A {
                 fun b() {}
@@ -1430,7 +1431,7 @@
     )
 
     @Test
-    fun testFunInterfaces(): Unit = checkApi(
+    fun testFunInterfaces() = checkApi(
         """
             fun interface A {
                 fun compute(value: Int): Unit
@@ -1463,7 +1464,7 @@
     )
 
     @Test
-    fun testComposableFunInterfaces(): Unit = checkApi(
+    fun testComposableFunInterfaces() = checkApi(
         """
             fun interface A {
                 @Composable fun compute(value: Int): Unit
@@ -1503,7 +1504,7 @@
     )
 
     @Test
-    fun testFunInterfaceWithInlineReturnType(): Unit = checkApi(
+    fun testFunInterfaceWithInlineReturnType() = checkApi(
         """
             inline class Color(val value: Int)
             fun interface A {
@@ -1548,7 +1549,7 @@
     )
 
     @Test
-    fun testComposableFunInterfaceWithInlineReturnType(): Unit = checkApi(
+    fun testComposableFunInterfaceWithInlineReturnType() = checkApi(
         """
             inline class Color(val value: Int)
             fun interface A {
@@ -1593,7 +1594,7 @@
     )
 
     @Test
-    fun testComposableMap(): Unit = codegen(
+    fun testComposableMap() = codegen(
         """
             class Repro {
                 private val composables = linkedMapOf<String, @Composable () -> Unit>()
@@ -1606,7 +1607,7 @@
     )
 
     @Test
-    fun testComposableColorFunInterfaceExample(): Unit = checkApi(
+    fun testComposableColorFunInterfaceExample() = checkApi(
         """
             import androidx.compose.material.Text
             import androidx.compose.ui.graphics.Color
diff --git a/compose/compiler/compiler-hosted/integration-tests/src/test/java/androidx/compose/compiler/plugins/kotlin/ComposerParamTransformTests.kt b/compose/compiler/compiler-hosted/integration-tests/src/test/java/androidx/compose/compiler/plugins/kotlin/ComposerParamTransformTests.kt
index 013903b..1dc65de 100644
--- a/compose/compiler/compiler-hosted/integration-tests/src/test/java/androidx/compose/compiler/plugins/kotlin/ComposerParamTransformTests.kt
+++ b/compose/compiler/compiler-hosted/integration-tests/src/test/java/androidx/compose/compiler/plugins/kotlin/ComposerParamTransformTests.kt
@@ -22,6 +22,7 @@
 import org.jetbrains.kotlin.ir.declarations.IrValueParameter
 import org.jetbrains.kotlin.ir.expressions.IrGetValue
 import org.jetbrains.kotlin.ir.visitors.IrElementVisitorVoid
+import org.junit.Assert.assertEquals
 import org.junit.Test
 
 class ComposerParamTransformTests : AbstractIrTransformTest() {
@@ -392,14 +393,9 @@
                 traceEventStart(<>, %changed, -1, <>)
               }
               Example({ %composer: Composer?, %changed: Int ->
-                %composer.startReplaceableGroup(<>)
-                sourceInformation(%composer, "C:Test.kt#2487m")
-                if (%changed and 0b1011 !== 0b0010 || !%composer.skipping) {
-                  Unit
-                } else {
-                  %composer.skipToGroupEnd()
-                }
-                %composer.endReplaceableGroup()
+                sourceInformationMarkerStart(%composer, <>, "C:Test.kt#2487m")
+                Unit
+                sourceInformationMarkerEnd(%composer)
               }, %composer, 0)
               if (isTraceInProgress()) {
                 traceEventEnd()
@@ -655,23 +651,13 @@
                       traceEventStart(<>, %dirty, -1, <>)
                     }
                     emit({ %composer: Composer?, %changed: Int ->
-                      %composer.startReplaceableGroup(<>)
-                      sourceInformation(%composer, "C<emit>:Test.kt#2487m")
-                      if (%changed and 0b1011 !== 0b0010 || !%composer.skipping) {
-                        emit({ %composer: Composer?, %changed: Int ->
-                          %composer.startReplaceableGroup(<>)
-                          sourceInformation(%composer, "C<compos...>:Test.kt#2487m")
-                          if (%changed and 0b1011 !== 0b0010 || !%composer.skipping) {
-                            composable(%composer, 0b1110 and %dirty)
-                          } else {
-                            %composer.skipToGroupEnd()
-                          }
-                          %composer.endReplaceableGroup()
-                        }, %composer, 0)
-                      } else {
-                        %composer.skipToGroupEnd()
-                      }
-                      %composer.endReplaceableGroup()
+                      sourceInformationMarkerStart(%composer, <>, "C<emit>:Test.kt#2487m")
+                      emit({ %composer: Composer?, %changed: Int ->
+                        sourceInformationMarkerStart(%composer, <>, "C<compos...>:Test.kt#2487m")
+                        composable(%composer, 0b1110 and %dirty)
+                        sourceInformationMarkerEnd(%composer)
+                      }, %composer, 0)
+                      sourceInformationMarkerEnd(%composer)
                     }, %composer, 0)
                     if (isTraceInProgress()) {
                       traceEventEnd()
diff --git a/compose/compiler/compiler-hosted/integration-tests/src/test/java/androidx/compose/compiler/plugins/kotlin/ContextReceiversTransformTests.kt b/compose/compiler/compiler-hosted/integration-tests/src/test/java/androidx/compose/compiler/plugins/kotlin/ContextReceiversTransformTests.kt
index fd5aa06..1845180 100644
--- a/compose/compiler/compiler-hosted/integration-tests/src/test/java/androidx/compose/compiler/plugins/kotlin/ContextReceiversTransformTests.kt
+++ b/compose/compiler/compiler-hosted/integration-tests/src/test/java/androidx/compose/compiler/plugins/kotlin/ContextReceiversTransformTests.kt
@@ -17,12 +17,24 @@
 package androidx.compose.compiler.plugins.kotlin
 
 import org.intellij.lang.annotations.Language
+import org.jetbrains.kotlin.config.CompilerConfiguration
 import org.jetbrains.kotlin.config.LanguageFeature
 import org.jetbrains.kotlin.config.LanguageVersionSettingsImpl
 import org.jetbrains.kotlin.config.languageVersionSettings
 import org.junit.Test
 
 class ContextReceiversTransformTests : AbstractIrTransformTest() {
+    override fun CompilerConfiguration.updateConfiguration() {
+        put(ComposeConfiguration.SOURCE_INFORMATION_ENABLED_KEY, true)
+        languageVersionSettings = LanguageVersionSettingsImpl(
+            languageVersion = languageVersionSettings.languageVersion,
+            apiVersion = languageVersionSettings.apiVersion,
+            specificFeatures = mapOf(
+                LanguageFeature.ContextReceivers to LanguageFeature.State.ENABLED
+            )
+        )
+    }
+
     private fun contextReceivers(
         @Language("kotlin")
         unchecked: String,
@@ -43,15 +55,6 @@
 
             fun used(x: Any?) {}
         """.trimIndent(),
-        applyExtraConfiguration = {
-            languageVersionSettings = LanguageVersionSettingsImpl(
-                languageVersion = languageVersionSettings.languageVersion,
-                apiVersion = languageVersionSettings.apiVersion,
-                specificFeatures = mapOf(
-                    LanguageFeature.ContextReceivers to LanguageFeature.State.ENABLED
-                )
-            )
-        }
     )
 
     @Test
diff --git a/compose/compiler/compiler-hosted/integration-tests/src/test/java/androidx/compose/compiler/plugins/kotlin/ControlFlowTransformTests.kt b/compose/compiler/compiler-hosted/integration-tests/src/test/java/androidx/compose/compiler/plugins/kotlin/ControlFlowTransformTests.kt
index 7911764..854b883 100644
--- a/compose/compiler/compiler-hosted/integration-tests/src/test/java/androidx/compose/compiler/plugins/kotlin/ControlFlowTransformTests.kt
+++ b/compose/compiler/compiler-hosted/integration-tests/src/test/java/androidx/compose/compiler/plugins/kotlin/ControlFlowTransformTests.kt
@@ -20,7 +20,6 @@
 import org.junit.Test
 
 class ControlFlowTransformTests : AbstractControlFlowTransformTests() {
-
     @Test
     fun testIfNonComposable(): Unit = controlFlow(
         """
@@ -303,25 +302,20 @@
                 }
                 A(%composer, 0)
                 M3({ %composer: Composer?, %changed: Int ->
-                  %composer.startReplaceableGroup(<>)
-                  sourceInformation(%composer, "C<A()>,<A()>:Test.kt")
-                  if (%changed and 0b1011 !== 0b0010 || !%composer.skipping) {
-                    A(%composer, 0)
-                    if (condition) {
-                      %composer.endToMarker(tmp0_marker)
-                      if (isTraceInProgress()) {
-                        traceEventEnd()
-                      }
-                      %composer.endRestartGroup()?.updateScope { %composer: Composer?, %force: Int ->
-                        Test(condition, %composer, updateChangedFlags(%changed or 0b0001))
-                      }
-                      return
+                  sourceInformationMarkerStart(%composer, <>, "C<A()>,<A()>:Test.kt")
+                  A(%composer, 0)
+                  if (condition) {
+                    %composer.endToMarker(tmp0_marker)
+                    if (isTraceInProgress()) {
+                      traceEventEnd()
                     }
-                    A(%composer, 0)
-                  } else {
-                    %composer.skipToGroupEnd()
+                    %composer.endRestartGroup()?.updateScope { %composer: Composer?, %force: Int ->
+                      Test(condition, %composer, updateChangedFlags(%changed or 0b0001))
+                    }
+                    return
                   }
-                  %composer.endReplaceableGroup()
+                  A(%composer, 0)
+                  sourceInformationMarkerEnd(%composer)
                 }, %composer, 0)
                 A(%composer, 0)
                 if (isTraceInProgress()) {
@@ -380,46 +374,36 @@
                 }
                 A(%composer, 0)
                 M3({ %composer: Composer?, %changed: Int ->
-                  %composer.startReplaceableGroup(<>)
-                  sourceInformation(%composer, "C<A()>,<A()>:Test.kt")
-                  if (%changed and 0b1011 !== 0b0010 || !%composer.skipping) {
-                    A(%composer, 0)
-                    if (a) {
-                      %composer.endToMarker(tmp0_marker)
-                      if (isTraceInProgress()) {
-                        traceEventEnd()
-                      }
-                      %composer.endRestartGroup()?.updateScope { %composer: Composer?, %force: Int ->
-                        Test(a, b, %composer, updateChangedFlags(%changed or 0b0001))
-                      }
-                      return
+                  sourceInformationMarkerStart(%composer, <>, "C<A()>,<A()>:Test.kt")
+                  A(%composer, 0)
+                  if (a) {
+                    %composer.endToMarker(tmp0_marker)
+                    if (isTraceInProgress()) {
+                      traceEventEnd()
                     }
-                    A(%composer, 0)
-                  } else {
-                    %composer.skipToGroupEnd()
+                    %composer.endRestartGroup()?.updateScope { %composer: Composer?, %force: Int ->
+                      Test(a, b, %composer, updateChangedFlags(%changed or 0b0001))
+                    }
+                    return
                   }
-                  %composer.endReplaceableGroup()
+                  A(%composer, 0)
+                  sourceInformationMarkerEnd(%composer)
                 }, %composer, 0)
                 M3({ %composer: Composer?, %changed: Int ->
-                  %composer.startReplaceableGroup(<>)
-                  sourceInformation(%composer, "C<A()>,<A()>:Test.kt")
-                  if (%changed and 0b1011 !== 0b0010 || !%composer.skipping) {
-                    A(%composer, 0)
-                    if (b) {
-                      %composer.endToMarker(tmp0_marker)
-                      if (isTraceInProgress()) {
-                        traceEventEnd()
-                      }
-                      %composer.endRestartGroup()?.updateScope { %composer: Composer?, %force: Int ->
-                        Test(a, b, %composer, updateChangedFlags(%changed or 0b0001))
-                      }
-                      return
+                  sourceInformationMarkerStart(%composer, <>, "C<A()>,<A()>:Test.kt")
+                  A(%composer, 0)
+                  if (b) {
+                    %composer.endToMarker(tmp0_marker)
+                    if (isTraceInProgress()) {
+                      traceEventEnd()
                     }
-                    A(%composer, 0)
-                  } else {
-                    %composer.skipToGroupEnd()
+                    %composer.endRestartGroup()?.updateScope { %composer: Composer?, %force: Int ->
+                      Test(a, b, %composer, updateChangedFlags(%changed or 0b0001))
+                    }
+                    return
                   }
-                  %composer.endReplaceableGroup()
+                  A(%composer, 0)
+                  sourceInformationMarkerEnd(%composer)
                 }, %composer, 0)
                 A(%composer, 0)
                 if (isTraceInProgress()) {
@@ -468,18 +452,13 @@
                 A(%composer, 0)
                 val tmp0_marker = %composer.currentMarker
                 M3({ %composer: Composer?, %changed: Int ->
-                  %composer.startReplaceableGroup(<>)
-                  sourceInformation(%composer, "C<A()>,<A()>:Test.kt")
-                  if (%changed and 0b1011 !== 0b0010 || !%composer.skipping) {
-                    A(%composer, 0)
-                    if (condition) {
-                      %composer.endToMarker(tmp0_marker)
-                    }
-                    A(%composer, 0)
-                  } else {
-                    %composer.skipToGroupEnd()
+                  sourceInformationMarkerStart(%composer, <>, "C<A()>,<A()>:Test.kt")
+                  A(%composer, 0)
+                  if (condition) {
+                    %composer.endToMarker(tmp0_marker)
                   }
-                  %composer.endReplaceableGroup()
+                  A(%composer, 0)
+                  sourceInformationMarkerEnd(%composer)
                 }, %composer, 0)
                 A(%composer, 0)
                 if (isTraceInProgress()) {
@@ -519,19 +498,14 @@
                       traceEventStart(<>, %changed, -1, <>)
                     }
                     M1({ %composer: Composer?, %changed: Int ->
-                      %composer.startReplaceableGroup(<>)
-                      sourceInformation(%composer, "C:Test.kt")
-                      if (%changed and 0b1011 !== 0b0010 || !%composer.skipping) {
-                        if (condition) {
-                          %composer.endToMarker(tmp0_marker)
-                          if (isTraceInProgress()) {
-                            traceEventEnd()
-                          }
+                      sourceInformationMarkerStart(%composer, <>, "C:Test.kt")
+                      if (condition) {
+                        %composer.endToMarker(tmp0_marker)
+                        if (isTraceInProgress()) {
+                          traceEventEnd()
                         }
-                      } else {
-                        %composer.skipToGroupEnd()
                       }
-                      %composer.endReplaceableGroup()
+                      sourceInformationMarkerEnd(%composer)
                     }, %composer, 0)
                     if (isTraceInProgress()) {
                       traceEventEnd()
@@ -594,28 +568,18 @@
                 }
                 A(%composer, 0)
                 M3({ %composer: Composer?, %changed: Int ->
-                  %composer.startReplaceableGroup(<>)
-                  sourceInformation(%composer, "C<A()>,<M1>,<A()>:Test.kt")
-                  if (%changed and 0b1011 !== 0b0010 || !%composer.skipping) {
-                    A(%composer, 0)
-                    val tmp0_marker = %composer.currentMarker
-                    M1({ %composer: Composer?, %changed: Int ->
-                      %composer.startReplaceableGroup(<>)
-                      sourceInformation(%composer, "C:Test.kt")
-                      if (%changed and 0b1011 !== 0b0010 || !%composer.skipping) {
-                        if (condition) {
-                          %composer.endToMarker(tmp0_marker)
-                        }
-                      } else {
-                        %composer.skipToGroupEnd()
-                      }
-                      %composer.endReplaceableGroup()
-                    }, %composer, 0)
-                    A(%composer, 0)
-                  } else {
-                    %composer.skipToGroupEnd()
-                  }
-                  %composer.endReplaceableGroup()
+                  sourceInformationMarkerStart(%composer, <>, "C<A()>,<M1>,<A()>:Test.kt")
+                  A(%composer, 0)
+                  val tmp0_marker = %composer.currentMarker
+                  M1({ %composer: Composer?, %changed: Int ->
+                    sourceInformationMarkerStart(%composer, <>, "C:Test.kt")
+                    if (condition) {
+                      %composer.endToMarker(tmp0_marker)
+                    }
+                    sourceInformationMarkerEnd(%composer)
+                  }, %composer, 0)
+                  A(%composer, 0)
+                  sourceInformationMarkerEnd(%composer)
                 }, %composer, 0)
                 A(%composer, 0)
                 if (isTraceInProgress()) {
@@ -665,27 +629,17 @@
                 A(%composer, 0)
                 val tmp0_marker = %composer.currentMarker
                 M3({ %composer: Composer?, %changed: Int ->
-                  %composer.startReplaceableGroup(<>)
-                  sourceInformation(%composer, "C<A()>,<M1>,<A()>:Test.kt")
-                  if (%changed and 0b1011 !== 0b0010 || !%composer.skipping) {
-                    A(%composer, 0)
-                    M1({ %composer: Composer?, %changed: Int ->
-                      %composer.startReplaceableGroup(<>)
-                      sourceInformation(%composer, "C:Test.kt")
-                      if (%changed and 0b1011 !== 0b0010 || !%composer.skipping) {
-                        if (condition) {
-                          %composer.endToMarker(tmp0_marker)
-                        }
-                      } else {
-                        %composer.skipToGroupEnd()
-                      }
-                      %composer.endReplaceableGroup()
-                    }, %composer, 0)
-                    A(%composer, 0)
-                  } else {
-                    %composer.skipToGroupEnd()
-                  }
-                  %composer.endReplaceableGroup()
+                  sourceInformationMarkerStart(%composer, <>, "C<A()>,<M1>,<A()>:Test.kt")
+                  A(%composer, 0)
+                  M1({ %composer: Composer?, %changed: Int ->
+                    sourceInformationMarkerStart(%composer, <>, "C:Test.kt")
+                    if (condition) {
+                      %composer.endToMarker(tmp0_marker)
+                    }
+                    sourceInformationMarkerEnd(%composer)
+                  }, %composer, 0)
+                  A(%composer, 0)
+                  sourceInformationMarkerEnd(%composer)
                 }, %composer, 0)
                 A(%composer, 0)
                 if (isTraceInProgress()) {
@@ -738,32 +692,27 @@
                 }
                 A(%composer, 0)
                 M1({ %composer: Composer?, %changed: Int ->
+                  sourceInformationMarkerStart(%composer, <>, "C<A()>,<A()>:Test.kt")
+                  A(%composer, 0)
                   %composer.startReplaceableGroup(<>)
-                  sourceInformation(%composer, "C<A()>,<A()>:Test.kt")
-                  if (%changed and 0b1011 !== 0b0010 || !%composer.skipping) {
+                  sourceInformation(%composer, "*<A()>,<A()>")
+                  while (true) {
                     A(%composer, 0)
-                    %composer.startReplaceableGroup(<>)
-                    sourceInformation(%composer, "*<A()>,<A()>")
-                    while (true) {
-                      A(%composer, 0)
-                      if (condition) {
-                        %composer.endToMarker(tmp0_marker)
-                        if (isTraceInProgress()) {
-                          traceEventEnd()
-                        }
-                        %composer.endRestartGroup()?.updateScope { %composer: Composer?, %force: Int ->
-                          testInline_M1_W_Return_Func(condition, %composer, updateChangedFlags(%changed or 0b0001))
-                        }
-                        return
+                    if (condition) {
+                      %composer.endToMarker(tmp0_marker)
+                      if (isTraceInProgress()) {
+                        traceEventEnd()
                       }
-                      A(%composer, 0)
+                      %composer.endRestartGroup()?.updateScope { %composer: Composer?, %force: Int ->
+                        testInline_M1_W_Return_Func(condition, %composer, updateChangedFlags(%changed or 0b0001))
+                      }
+                      return
                     }
-                    %composer.endReplaceableGroup()
                     A(%composer, 0)
-                  } else {
-                    %composer.skipToGroupEnd()
                   }
                   %composer.endReplaceableGroup()
+                  A(%composer, 0)
+                  sourceInformationMarkerEnd(%composer)
                 }, %composer, 0)
                 A(%composer, 0)
                 if (isTraceInProgress()) {
@@ -819,33 +768,23 @@
                 A(%composer, 0)
                 val tmp0_marker = %composer.currentMarker
                 M3({ %composer: Composer?, %changed: Int ->
-                  %composer.startReplaceableGroup(<>)
-                  sourceInformation(%composer, "C<A()>,<A()>:Test.kt")
-                  if (%changed and 0b1011 !== 0b0010 || !%composer.skipping) {
-                    A(%composer, 0)
-                    if (condition) {
-                      %composer.endToMarker(tmp0_marker)
-                    }
-                    A(%composer, 0)
-                  } else {
-                    %composer.skipToGroupEnd()
+                  sourceInformationMarkerStart(%composer, <>, "C<A()>,<A()>:Test.kt")
+                  A(%composer, 0)
+                  if (condition) {
+                    %composer.endToMarker(tmp0_marker)
                   }
-                  %composer.endReplaceableGroup()
+                  A(%composer, 0)
+                  sourceInformationMarkerEnd(%composer)
                 }, %composer, 0)
                 val tmp1_marker = %composer.currentMarker
                 M3({ %composer: Composer?, %changed: Int ->
-                  %composer.startReplaceableGroup(<>)
-                  sourceInformation(%composer, "C<A()>,<A()>:Test.kt")
-                  if (%changed and 0b1011 !== 0b0010 || !%composer.skipping) {
-                    A(%composer, 0)
-                    if (condition) {
-                      %composer.endToMarker(tmp1_marker)
-                    }
-                    A(%composer, 0)
-                  } else {
-                    %composer.skipToGroupEnd()
+                  sourceInformationMarkerStart(%composer, <>, "C<A()>,<A()>:Test.kt")
+                  A(%composer, 0)
+                  if (condition) {
+                    %composer.endToMarker(tmp1_marker)
                   }
-                  %composer.endReplaceableGroup()
+                  A(%composer, 0)
+                  sourceInformationMarkerEnd(%composer)
                 }, %composer, 0)
                 A(%composer, 0)
                 if (isTraceInProgress()) {
@@ -898,39 +837,29 @@
                 }
                 Text("Root - before", %composer, 0b0110)
                 M1({ %composer: Composer?, %changed: Int ->
+                  sourceInformationMarkerStart(%composer, <>, "C<Text("...>,<Text("...>:Test.kt")
+                  Text("M1 - begin", %composer, 0b0110)
                   %composer.startReplaceableGroup(<>)
-                  sourceInformation(%composer, "C<Text("...>,<Text("...>:Test.kt")
-                  if (%changed and 0b1011 !== 0b0010 || !%composer.skipping) {
-                    Text("M1 - begin", %composer, 0b0110)
-                    %composer.startReplaceableGroup(<>)
-                    sourceInformation(%composer, "<Text("...>,<M1>")
-                    if (condition) {
-                      Text("if - begin", %composer, 0b0110)
-                      M1({ %composer: Composer?, %changed: Int ->
-                        %composer.startReplaceableGroup(<>)
-                        sourceInformation(%composer, "C<Text("...>:Test.kt")
-                        if (%changed and 0b1011 !== 0b0010 || !%composer.skipping) {
-                          Text("In CCM1", %composer, 0b0110)
-                          %composer.endToMarker(tmp0_marker)
-                          if (isTraceInProgress()) {
-                            traceEventEnd()
-                          }
-                          %composer.endRestartGroup()?.updateScope { %composer: Composer?, %force: Int ->
-                            test_CM1_CCM1_RetFun(condition, %composer, updateChangedFlags(%changed or 0b0001))
-                          }
-                          return
-                        } else {
-                          %composer.skipToGroupEnd()
-                        }
-                        %composer.endReplaceableGroup()
-                      }, %composer, 0)
-                    }
-                    %composer.endReplaceableGroup()
-                    Text("M1 - end", %composer, 0b0110)
-                  } else {
-                    %composer.skipToGroupEnd()
+                  sourceInformation(%composer, "<Text("...>,<M1>")
+                  if (condition) {
+                    Text("if - begin", %composer, 0b0110)
+                    M1({ %composer: Composer?, %changed: Int ->
+                      sourceInformationMarkerStart(%composer, <>, "C<Text("...>:Test.kt")
+                      Text("In CCM1", %composer, 0b0110)
+                      %composer.endToMarker(tmp0_marker)
+                      if (isTraceInProgress()) {
+                        traceEventEnd()
+                      }
+                      %composer.endRestartGroup()?.updateScope { %composer: Composer?, %force: Int ->
+                        test_CM1_CCM1_RetFun(condition, %composer, updateChangedFlags(%changed or 0b0001))
+                      }
+                      return
+                      sourceInformationMarkerEnd(%composer)
+                    }, %composer, 0)
                   }
                   %composer.endReplaceableGroup()
+                  Text("M1 - end", %composer, 0b0110)
+                  sourceInformationMarkerEnd(%composer)
                 }, %composer, 0)
                 Text("Root - end", %composer, 0b0110)
                 if (isTraceInProgress()) {
@@ -975,17 +904,12 @@
               }
               val tmp0_marker = %composer.currentMarker
               FakeBox({ %composer: Composer?, %changed: Int ->
-                %composer.startReplaceableGroup(<>)
-                sourceInformation(%composer, "C<A()>:Test.kt")
-                if (%changed and 0b1011 !== 0b0010 || !%composer.skipping) {
-                  if (condition) {
-                    %composer.endToMarker(tmp0_marker)
-                  }
-                  A(%composer, 0)
-                } else {
-                  %composer.skipToGroupEnd()
+                sourceInformationMarkerStart(%composer, <>, "C<A()>:Test.kt")
+                if (condition) {
+                  %composer.endToMarker(tmp0_marker)
                 }
-                %composer.endReplaceableGroup()
+                A(%composer, 0)
+                sourceInformationMarkerEnd(%composer)
               }, %composer, 0)
               if (isTraceInProgress()) {
                 traceEventEnd()
@@ -1153,17 +1077,12 @@
                 }
                 val tmp0_marker = %composer.currentMarker
                 IW({ %composer: Composer?, %changed: Int ->
-                  %composer.startReplaceableGroup(<>)
-                  sourceInformation(%composer, "C<A()>:Test.kt")
-                  if (%changed and 0b1011 !== 0b0010 || !%composer.skipping) {
-                    if (condition) {
-                      %composer.endToMarker(tmp0_marker)
-                    }
-                    A(%composer, 0)
-                  } else {
-                    %composer.skipToGroupEnd()
+                  sourceInformationMarkerStart(%composer, <>, "C<A()>:Test.kt")
+                  if (condition) {
+                    %composer.endToMarker(tmp0_marker)
                   }
-                  %composer.endReplaceableGroup()
+                  A(%composer, 0)
+                  sourceInformationMarkerEnd(%composer)
                 }, %composer, 0)
                 if (isTraceInProgress()) {
                   traceEventEnd()
@@ -1259,25 +1178,20 @@
                 }
                 Text("Some text", %composer, 0b0110)
                 M1({ %composer: Composer?, %changed: Int ->
-                  %composer.startReplaceableGroup(<>)
-                  sourceInformation(%composer, "C:Test.kt")
-                  if (%changed and 0b1011 !== 0b0010 || !%composer.skipping) {
-                    Identity {
-                      if (condition) {
-                        %composer.endToMarker(tmp0_marker)
-                        if (isTraceInProgress()) {
-                          traceEventEnd()
-                        }
-                        %composer.endRestartGroup()?.updateScope { %composer: Composer?, %force: Int ->
-                          Test(condition, %composer, updateChangedFlags(%changed or 0b0001))
-                        }
-                        return
+                  sourceInformationMarkerStart(%composer, <>, "C:Test.kt")
+                  Identity {
+                    if (condition) {
+                      %composer.endToMarker(tmp0_marker)
+                      if (isTraceInProgress()) {
+                        traceEventEnd()
                       }
+                      %composer.endRestartGroup()?.updateScope { %composer: Composer?, %force: Int ->
+                        Test(condition, %composer, updateChangedFlags(%changed or 0b0001))
+                      }
+                      return
                     }
-                  } else {
-                    %composer.skipToGroupEnd()
                   }
-                  %composer.endReplaceableGroup()
+                  sourceInformationMarkerEnd(%composer)
                 }, %composer, 0)
                 Text("Some more text", %composer, 0b0110)
                 if (isTraceInProgress()) {
@@ -1323,18 +1237,13 @@
                 Text("Some text", %composer, 0b0110)
                 val tmp0_marker = %composer.currentMarker
                 M1({ %composer: Composer?, %changed: Int ->
-                  %composer.startReplaceableGroup(<>)
-                  sourceInformation(%composer, "C:Test.kt")
-                  if (%changed and 0b1011 !== 0b0010 || !%composer.skipping) {
-                    Identity {
-                      if (condition) {
-                        %composer.endToMarker(tmp0_marker)
-                      }
+                  sourceInformationMarkerStart(%composer, <>, "C:Test.kt")
+                  Identity {
+                    if (condition) {
+                      %composer.endToMarker(tmp0_marker)
                     }
-                  } else {
-                    %composer.skipToGroupEnd()
                   }
-                  %composer.endReplaceableGroup()
+                  sourceInformationMarkerEnd(%composer)
                 }, %composer, 0)
                 Text("Some more text", %composer, 0b0110)
                 if (isTraceInProgress()) {
@@ -1351,7 +1260,7 @@
     )
 
     @Test
-    fun testEnsureRuntimeTestWillCompile_CL() = ensureSetup {
+    fun testEnsureRuntimeTestWillCompile_CL() {
         classLoader(
             """
             import androidx.compose.runtime.Composable
@@ -1412,25 +1321,20 @@
                 }
                 Text("Root - before", %composer, 0b0110)
                 M1({ %composer: Composer?, %changed: Int ->
-                  %composer.startReplaceableGroup(<>)
-                  sourceInformation(%composer, "C<Text("...>,<Text("...>:Test.kt")
-                  if (%changed and 0b1011 !== 0b0010 || !%composer.skipping) {
-                    Text("M1 - before", %composer, 0b0110)
-                    if (condition) {
-                      %composer.endToMarker(tmp0_marker)
-                      if (isTraceInProgress()) {
-                        traceEventEnd()
-                      }
-                      %composer.endRestartGroup()?.updateScope { %composer: Composer?, %force: Int ->
-                        test_CM1_RetFun(condition, %composer, updateChangedFlags(%changed or 0b0001))
-                      }
-                      return
+                  sourceInformationMarkerStart(%composer, <>, "C<Text("...>,<Text("...>:Test.kt")
+                  Text("M1 - before", %composer, 0b0110)
+                  if (condition) {
+                    %composer.endToMarker(tmp0_marker)
+                    if (isTraceInProgress()) {
+                      traceEventEnd()
                     }
-                    Text("M1 - after", %composer, 0b0110)
-                  } else {
-                    %composer.skipToGroupEnd()
+                    %composer.endRestartGroup()?.updateScope { %composer: Composer?, %force: Int ->
+                      test_CM1_RetFun(condition, %composer, updateChangedFlags(%changed or 0b0001))
+                    }
+                    return
                   }
-                  %composer.endReplaceableGroup()
+                  Text("M1 - after", %composer, 0b0110)
+                  sourceInformationMarkerEnd(%composer)
                 }, %composer, 0)
                 Text("Root - after", %composer, 0b0110)
                 if (isTraceInProgress()) {
@@ -4156,14 +4060,9 @@
                   traceEventStart(<>, %changed, -1, <>)
                 }
                 IW({ %composer: Composer?, %changed: Int ->
-                  %composer.startReplaceableGroup(<>)
-                  sourceInformation(%composer, "C<A()>:Test.kt")
-                  if (%changed and 0b1011 !== 0b0010 || !%composer.skipping) {
-                    A(%composer, 0)
-                  } else {
-                    %composer.skipToGroupEnd()
-                  }
-                  %composer.endReplaceableGroup()
+                  sourceInformationMarkerStart(%composer, <>, "C<A()>:Test.kt")
+                  A(%composer, 0)
+                  sourceInformationMarkerEnd(%composer)
                 }, %composer, 0)
                 if (isTraceInProgress()) {
                   traceEventEnd()
@@ -5502,21 +5401,16 @@
                     traceEventStart(<>, %changed, -1, "ComposableSingletons%TestKt.lambda-1.<anonymous> (Test.kt:5)")
                   }
                   IW({ %composer: Composer?, %changed: Int ->
+                    sourceInformationMarkerStart(%composer, <>, "C<T(2)>,<T(4)>:Test.kt")
+                    T(2, %composer, 0b0110)
                     %composer.startReplaceableGroup(<>)
-                    sourceInformation(%composer, "C<T(2)>,<T(4)>:Test.kt")
-                    if (%changed and 0b1011 !== 0b0010 || !%composer.skipping) {
-                      T(2, %composer, 0b0110)
-                      %composer.startReplaceableGroup(<>)
-                      sourceInformation(%composer, "*<T(3)>")
-                      repeat(3) { it: Int ->
-                        T(3, %composer, 0b0110)
-                      }
-                      %composer.endReplaceableGroup()
-                      T(4, %composer, 0b0110)
-                    } else {
-                      %composer.skipToGroupEnd()
+                    sourceInformation(%composer, "*<T(3)>")
+                    repeat(3) { it: Int ->
+                      T(3, %composer, 0b0110)
                     }
                     %composer.endReplaceableGroup()
+                    T(4, %composer, 0b0110)
+                    sourceInformationMarkerEnd(%composer)
                   }, %composer, 0)
                   if (isTraceInProgress()) {
                     traceEventEnd()
@@ -5596,14 +5490,9 @@
                 val c = current
                 val cl = calculateSometing(%composer, 0)
                 Layout({ %composer: Composer?, %changed: Int ->
-                  %composer.startReplaceableGroup(<>)
-                  sourceInformation(%composer, "C<Text("...>:Test.kt")
-                  if (%changed and 0b1011 !== 0b0010 || !%composer.skipping) {
-                    Text("%c %cl", %composer, 0)
-                  } else {
-                    %composer.skipToGroupEnd()
-                  }
-                  %composer.endReplaceableGroup()
+                  sourceInformationMarkerStart(%composer, <>, "C<Text("...>:Test.kt")
+                  Text("%c %cl", %composer, 0)
+                  sourceInformationMarkerEnd(%composer)
                 }, %composer, 0)
                 if (isTraceInProgress()) {
                   traceEventEnd()
@@ -5653,14 +5542,14 @@
 
             @Composable
             @ReadOnlyComposable
-            fun calculateSometing(): Int {
+            fun calculateSomething(): Int {
                 return 0;
             }
 
             @Composable
             fun Test() {
                 val c = holderHolder.current
-                val cl = calculateSometing()
+                val cl = calculateSomething()
                 Layout {
                     Text("${'$'}c ${'$'}cl")
                 }
@@ -5701,10 +5590,10 @@
             val holderHolder: HolderHolder = HolderHolder()
             @Composable
             @ReadOnlyComposable
-            fun calculateSometing(%composer: Composer?, %changed: Int): Int {
-              sourceInformationMarkerStart(%composer, <>, "C(calculateSometing):Test.kt")
+            fun calculateSomething(%composer: Composer?, %changed: Int): Int {
+              sourceInformationMarkerStart(%composer, <>, "C(calculateSomething):Test.kt")
               if (isTraceInProgress()) {
-                traceEventStart(<>, %changed, -1, "calculateSometing (Test.kt:23)")
+                traceEventStart(<>, %changed, -1, "calculateSomething (Test.kt:23)")
               }
               val tmp0 = 0
               if (isTraceInProgress()) {
@@ -5722,16 +5611,11 @@
                   traceEventStart(<>, %changed, -1, "Test (Test.kt:28)")
                 }
                 val c = holderHolder.current
-                val cl = calculateSometing(%composer, 0)
+                val cl = calculateSomething(%composer, 0)
                 Layout({ %composer: Composer?, %changed: Int ->
-                  %composer.startReplaceableGroup(<>)
-                  sourceInformation(%composer, "C<Text("...>:Test.kt")
-                  if (%changed and 0b1011 !== 0b0010 || !%composer.skipping) {
-                    Text("%c %cl", %composer, 0)
-                  } else {
-                    %composer.skipToGroupEnd()
-                  }
-                  %composer.endReplaceableGroup()
+                  sourceInformationMarkerStart(%composer, <>, "C<Text("...>:Test.kt")
+                  Text("%c %cl", %composer, 0)
+                  sourceInformationMarkerEnd(%composer)
                 }, %composer, 0)
                 if (isTraceInProgress()) {
                   traceEventEnd()
@@ -5816,22 +5700,17 @@
                   traceEventStart(<>, %changed, -1, <>)
                 }
                 Wrapper({ %composer: Composer?, %changed: Int ->
-                  %composer.startReplaceableGroup(<>)
-                  sourceInformation(%composer, "C*<Leaf(0...>:Test.kt")
-                  if (%changed and 0b1011 !== 0b0010 || !%composer.skipping) {
+                  sourceInformationMarkerStart(%composer, <>, "C*<Leaf(0...>:Test.kt")
+                  repeat(1) { it: Int ->
+                    %composer.startReplaceableGroup(<>)
+                    sourceInformation(%composer, "*<Leaf(0...>")
                     repeat(1) { it: Int ->
-                      %composer.startReplaceableGroup(<>)
-                      sourceInformation(%composer, "*<Leaf(0...>")
-                      repeat(1) { it: Int ->
-                        Leaf(0, %composer, 0b0110, 0)
-                      }
-                      %composer.endReplaceableGroup()
                       Leaf(0, %composer, 0b0110, 0)
                     }
-                  } else {
-                    %composer.skipToGroupEnd()
+                    %composer.endReplaceableGroup()
+                    Leaf(0, %composer, 0b0110, 0)
                   }
-                  %composer.endReplaceableGroup()
+                  sourceInformationMarkerEnd(%composer)
                 }, %composer, 0)
                 if (isTraceInProgress()) {
                   traceEventEnd()
diff --git a/compose/compiler/compiler-hosted/integration-tests/src/test/java/androidx/compose/compiler/plugins/kotlin/ControlFlowTransformTestsNoSource.kt b/compose/compiler/compiler-hosted/integration-tests/src/test/java/androidx/compose/compiler/plugins/kotlin/ControlFlowTransformTestsNoSource.kt
index 1f5756b..7a04e15 100644
--- a/compose/compiler/compiler-hosted/integration-tests/src/test/java/androidx/compose/compiler/plugins/kotlin/ControlFlowTransformTestsNoSource.kt
+++ b/compose/compiler/compiler-hosted/integration-tests/src/test/java/androidx/compose/compiler/plugins/kotlin/ControlFlowTransformTestsNoSource.kt
@@ -16,10 +16,13 @@
 
 package androidx.compose.compiler.plugins.kotlin
 
+import org.jetbrains.kotlin.config.CompilerConfiguration
 import org.junit.Test
 
 class ControlFlowTransformTestsNoSource : AbstractControlFlowTransformTests() {
-    override val sourceInformationEnabled: Boolean get() = false
+    override fun CompilerConfiguration.updateConfiguration() {
+        put(ComposeConfiguration.SOURCE_INFORMATION_ENABLED_KEY, false)
+    }
 
     @Test
     fun testPublicFunctionAlwaysMarkedAsCall(): Unit = controlFlow(
@@ -155,13 +158,7 @@
                   traceEventStart(<>, %changed, -1, <>)
                 }
                 IW({ %composer: Composer?, %changed: Int ->
-                  %composer.startReplaceableGroup(<>)
-                  if (%changed and 0b1011 !== 0b0010 || !%composer.skipping) {
-                    A(%composer, 0)
-                  } else {
-                    %composer.skipToGroupEnd()
-                  }
-                  %composer.endReplaceableGroup()
+                  A(%composer, 0)
                 }, %composer, 0)
                 if (isTraceInProgress()) {
                   traceEventEnd()
diff --git a/compose/compiler/compiler-hosted/integration-tests/src/test/java/androidx/compose/compiler/plugins/kotlin/DefaultParamTransformTests.kt b/compose/compiler/compiler-hosted/integration-tests/src/test/java/androidx/compose/compiler/plugins/kotlin/DefaultParamTransformTests.kt
index 07fd352..d0a6e48 100644
--- a/compose/compiler/compiler-hosted/integration-tests/src/test/java/androidx/compose/compiler/plugins/kotlin/DefaultParamTransformTests.kt
+++ b/compose/compiler/compiler-hosted/integration-tests/src/test/java/androidx/compose/compiler/plugins/kotlin/DefaultParamTransformTests.kt
@@ -197,14 +197,9 @@
         """,
         """
             fun Bar(unused: Function2<Composer, Int, Unit> = { %composer: Composer?, %changed: Int ->
-              %composer.startReplaceableGroup(<>)
-              sourceInformation(%composer, "C:Test.kt")
-              if (%changed and 0b1011 !== 0b0010 || !%composer.skipping) {
-                Unit
-              } else {
-                %composer.skipToGroupEnd()
-              }
-              %composer.endReplaceableGroup()
+              sourceInformationMarkerStart(%composer, <>, "C:Test.kt")
+              Unit
+              sourceInformationMarkerEnd(%composer)
             }
             ) { }
             fun Foo() {
diff --git a/compose/compiler/compiler-hosted/integration-tests/src/test/java/androidx/compose/compiler/plugins/kotlin/DurableFunctionKeyCodegenTests.kt b/compose/compiler/compiler-hosted/integration-tests/src/test/java/androidx/compose/compiler/plugins/kotlin/DurableFunctionKeyCodegenTests.kt
index 43746a5..359393d 100644
--- a/compose/compiler/compiler-hosted/integration-tests/src/test/java/androidx/compose/compiler/plugins/kotlin/DurableFunctionKeyCodegenTests.kt
+++ b/compose/compiler/compiler-hosted/integration-tests/src/test/java/androidx/compose/compiler/plugins/kotlin/DurableFunctionKeyCodegenTests.kt
@@ -17,6 +17,7 @@
 package androidx.compose.compiler.plugins.kotlin
 
 import org.jetbrains.kotlin.config.CompilerConfiguration
+import org.junit.Assert.assertEquals
 import org.junit.Test
 import org.junit.runner.RunWith
 import org.robolectric.RobolectricTestRunner
@@ -30,10 +31,8 @@
     maxSdk = 23
 )
 class DurableFunctionKeyCodegenTests : AbstractCodegenSignatureTest() {
-
-    override fun updateConfiguration(configuration: CompilerConfiguration) {
-        super.updateConfiguration(configuration)
-        configuration.put(ComposeConfiguration.GENERATE_FUNCTION_KEY_META_CLASSES_KEY, true)
+    override fun CompilerConfiguration.updateConfiguration() {
+        put(ComposeConfiguration.GENERATE_FUNCTION_KEY_META_CLASSES_KEY, true)
     }
 
     @Test
diff --git a/compose/compiler/compiler-hosted/integration-tests/src/test/java/androidx/compose/compiler/plugins/kotlin/FcsTypeResolutionTests.kt b/compose/compiler/compiler-hosted/integration-tests/src/test/java/androidx/compose/compiler/plugins/kotlin/FcsTypeResolutionTests.kt
index 8b14e7a..5efb490 100644
--- a/compose/compiler/compiler-hosted/integration-tests/src/test/java/androidx/compose/compiler/plugins/kotlin/FcsTypeResolutionTests.kt
+++ b/compose/compiler/compiler-hosted/integration-tests/src/test/java/androidx/compose/compiler/plugins/kotlin/FcsTypeResolutionTests.kt
@@ -16,9 +16,11 @@
 
 package androidx.compose.compiler.plugins.kotlin
 
-class FcsTypeResolutionTests : AbstractComposeDiagnosticsTest() {
+import org.junit.Test
 
-    fun testImplicitlyPassedReceiverScope1() = doTest(
+class FcsTypeResolutionTests : AbstractComposeDiagnosticsTest() {
+    @Test
+    fun testImplicitlyPassedReceiverScope1() = check(
         """
             import androidx.compose.runtime.*
 
@@ -29,7 +31,8 @@
         """
     )
 
-    fun testImplicitlyPassedReceiverScope2() = doTest(
+    @Test
+    fun testImplicitlyPassedReceiverScope2() = check(
         """
             import androidx.compose.runtime.*
 
@@ -45,7 +48,8 @@
         """
     )
 
-    fun testSmartCastsAndPunning() = doTest(
+    @Test
+    fun testSmartCastsAndPunning() = check(
         """
             import androidx.compose.runtime.*
 
@@ -63,7 +67,8 @@
         """
     )
 
-    fun testExtensionInvoke() = doTest(
+    @Test
+    fun testExtensionInvoke() = check(
         """
             import androidx.compose.runtime.*
 
@@ -76,7 +81,8 @@
         """
     )
 
-    fun testResolutionInsideWhenExpression() = doTest(
+    @Test
+    fun testResolutionInsideWhenExpression() = check(
         """
             import androidx.compose.runtime.*
             
@@ -91,7 +97,8 @@
         """
     )
 
-    fun testUsedParameters() = doTest(
+    @Test
+    fun testUsedParameters() = check(
         """
             import androidx.compose.runtime.*
             import android.widget.LinearLayout
@@ -134,7 +141,8 @@
         """
     )
 
-    fun testDispatchInvoke() = doTest(
+    @Test
+    fun testDispatchInvoke() = check(
         """
             import androidx.compose.runtime.*
 
@@ -150,7 +158,8 @@
         """
     )
 
-    fun testDispatchAndExtensionReceiver() = doTest(
+    @Test
+    fun testDispatchAndExtensionReceiver() = check(
         """
             import androidx.compose.runtime.*
 
@@ -168,7 +177,8 @@
         """
     )
 
-    fun testDispatchAndExtensionReceiverLocal() = doTest(
+    @Test
+    fun testDispatchAndExtensionReceiverLocal() = check(
         """
             import androidx.compose.runtime.*
 
@@ -184,7 +194,8 @@
         """
     )
 
-    fun testMissingAttributes() = doTest(
+    @Test
+    fun testMissingAttributes() = check(
         """
             import androidx.compose.runtime.*
 
@@ -213,7 +224,8 @@
         """.trimIndent()
     )
 
-    fun testDuplicateAttributes() = doTest(
+    @Test
+    fun testDuplicateAttributes() = check(
         """
             import androidx.compose.runtime.*
 
@@ -232,7 +244,8 @@
         """.trimIndent()
     )
 
-    fun testChildrenNamedAndBodyDuplicate() = doTest(
+    @Test
+    fun testChildrenNamedAndBodyDuplicate() = check(
         """
             import androidx.compose.runtime.*
 
@@ -245,7 +258,8 @@
         """.trimIndent()
     )
 
-    fun testAbstractClassTags() = doTest(
+    @Test
+    fun testAbstractClassTags() = check(
         """
             import androidx.compose.runtime.*
             import android.content.Context
@@ -263,7 +277,8 @@
         """.trimIndent()
     )
 
-    fun testGenerics() = doTest(
+    @Test
+    fun testGenerics() = check(
         """
             import androidx.compose.runtime.*
 
@@ -298,7 +313,8 @@
         """.trimIndent()
     )
 
-    fun testUnresolvedAttributeValueResolvedTarget() = doTest(
+    @Test
+    fun testUnresolvedAttributeValueResolvedTarget() = check(
         """
             import androidx.compose.runtime.*
 
@@ -331,7 +347,8 @@
     )
 
     // TODO(lmr): this triggers an exception!
-    fun testEmptyAttributeValue() = doTest(
+    @Test
+    fun testEmptyAttributeValue() = check(
         """
             import androidx.compose.runtime.*
 
@@ -349,10 +366,12 @@
                 Foo(abc=123, xyz=)
             }
 
-        """.trimIndent()
+        """.trimIndent(),
+        ignoreParseErrors = true
     )
 
-    fun testMismatchedAttributes() = doTest(
+    @Test
+    fun testMismatchedAttributes() = check(
         """
             import androidx.compose.runtime.*
 
@@ -386,7 +405,8 @@
         """.trimIndent()
     )
 
-    fun testErrorAttributeValue() = doTest(
+    @Test
+    fun testErrorAttributeValue() = check(
         """
             import androidx.compose.runtime.*
 
@@ -402,7 +422,8 @@
         """.trimIndent()
     )
 
-    fun testUnresolvedQualifiedTag() = doTest(
+    @Test
+    fun testUnresolvedQualifiedTag() = check(
         """
             import androidx.compose.runtime.*
 
@@ -460,7 +481,8 @@
     )
 
     // TODO(lmr): overloads creates resolution exception
-    fun testChildren() = doTest(
+    @Test
+    fun testChildren() = check(
         """
             import androidx.compose.runtime.*
             import android.widget.Button
diff --git a/compose/compiler/compiler-hosted/integration-tests/src/test/java/androidx/compose/compiler/plugins/kotlin/FunctionBodySkippingTransformTests.kt b/compose/compiler/compiler-hosted/integration-tests/src/test/java/androidx/compose/compiler/plugins/kotlin/FunctionBodySkippingTransformTests.kt
index 4b2e6df..b4e644c 100644
--- a/compose/compiler/compiler-hosted/integration-tests/src/test/java/androidx/compose/compiler/plugins/kotlin/FunctionBodySkippingTransformTests.kt
+++ b/compose/compiler/compiler-hosted/integration-tests/src/test/java/androidx/compose/compiler/plugins/kotlin/FunctionBodySkippingTransformTests.kt
@@ -17,6 +17,7 @@
 package androidx.compose.compiler.plugins.kotlin
 
 import org.intellij.lang.annotations.Language
+import org.jetbrains.kotlin.config.CompilerConfiguration
 import org.junit.Test
 
 abstract class FunctionBodySkippingTransformTestsBase : AbstractIrTransformTest() {
@@ -47,7 +48,6 @@
 }
 
 class FunctionBodySkippingTransformTests : FunctionBodySkippingTransformTestsBase() {
-
     @Test
     fun testIfInLambda(): Unit = comparisonPropagation(
         """
@@ -3906,18 +3906,9 @@
                   traceEventStart(<>, %changed, -1, <>)
                 }
                 Bug(listOf(1, 2, 3), { it: Int, %composer: Composer?, %changed: Int ->
-                  %composer.startReplaceableGroup(<>)
-                  sourceInformation(%composer, "C<Text(i...>:Test.kt")
-                  val %dirty = %changed
-                  if (%changed and 0b1110 === 0) {
-                    %dirty = %dirty or if (%composer.changed(it)) 0b0100 else 0b0010
-                  }
-                  if (%dirty and 0b01011011 !== 0b00010010 || !%composer.skipping) {
-                    Text(it.toString(), %composer, 0)
-                  } else {
-                    %composer.skipToGroupEnd()
-                  }
-                  %composer.endReplaceableGroup()
+                  sourceInformationMarkerStart(%composer, <>, "C<Text(i...>:Test.kt")
+                  Text(it.toString(), %composer, 0)
+                  sourceInformationMarkerEnd(%composer)
                 }, %composer, 0b0110)
                 if (isTraceInProgress()) {
                   traceEventEnd()
@@ -3952,7 +3943,9 @@
 }
 
 class FunctionBodySkippingTransformTestsNoSource : FunctionBodySkippingTransformTestsBase() {
-    override val sourceInformationEnabled: Boolean get() = false
+    override fun CompilerConfiguration.updateConfiguration() {
+        put(ComposeConfiguration.SOURCE_INFORMATION_ENABLED_KEY, false)
+    }
 
     @Test
     fun testGrouplessProperty(): Unit = comparisonPropagation(
@@ -4039,17 +4032,7 @@
                   traceEventStart(<>, %changed, -1, <>)
                 }
                 Bug(listOf(1, 2, 3), { it: Int, %composer: Composer?, %changed: Int ->
-                  %composer.startReplaceableGroup(<>)
-                  val %dirty = %changed
-                  if (%changed and 0b1110 === 0) {
-                    %dirty = %dirty or if (%composer.changed(it)) 0b0100 else 0b0010
-                  }
-                  if (%dirty and 0b01011011 !== 0b00010010 || !%composer.skipping) {
-                    Text(it.toString(), %composer, 0)
-                  } else {
-                    %composer.skipToGroupEnd()
-                  }
-                  %composer.endReplaceableGroup()
+                  Text(it.toString(), %composer, 0)
                 }, %composer, 0b0110)
                 if (isTraceInProgress()) {
                   traceEventEnd()
@@ -4080,4 +4063,52 @@
             fun Text(value: String) {}
         """
     )
+
+    @Test
+    fun test_InlineSkipping() = verifyComposeIrTransform(
+        source = """
+            import androidx.compose.runtime.*
+
+            @Composable
+            fun Test() {
+                InlineWrapperParam {
+                    Text("Function ${'$'}it")
+                }
+            }
+        """,
+        extra = """
+            import androidx.compose.runtime.*
+
+            @Composable
+            inline fun InlineWrapperParam(content: @Composable (Int) -> Unit) {
+                content(100)
+            }
+
+            @Composable
+            fun Text(text: String) { }
+        """,
+        expectedTransformed = """
+            @Composable
+            fun Test(%composer: Composer?, %changed: Int) {
+              %composer = %composer.startRestartGroup(<>)
+              sourceInformation(%composer, "C(Test)")
+              if (%changed !== 0 || !%composer.skipping) {
+                if (isTraceInProgress()) {
+                  traceEventStart(<>, %changed, -1, <>)
+                }
+                InlineWrapperParam({ it: Int, %composer: Composer?, %changed: Int ->
+                  Text("Function %it", %composer, 0)
+                }, %composer, 0)
+                if (isTraceInProgress()) {
+                  traceEventEnd()
+                }
+              } else {
+                %composer.skipToGroupEnd()
+              }
+              %composer.endRestartGroup()?.updateScope { %composer: Composer?, %force: Int ->
+                Test(%composer, updateChangedFlags(%changed or 0b0001))
+              }
+            }
+        """
+    )
 }
diff --git a/compose/compiler/compiler-hosted/integration-tests/src/test/java/androidx/compose/compiler/plugins/kotlin/GenerationUtils.kt b/compose/compiler/compiler-hosted/integration-tests/src/test/java/androidx/compose/compiler/plugins/kotlin/GenerationUtils.kt
deleted file mode 100644
index d77cf5d..0000000
--- a/compose/compiler/compiler-hosted/integration-tests/src/test/java/androidx/compose/compiler/plugins/kotlin/GenerationUtils.kt
+++ /dev/null
@@ -1,64 +0,0 @@
-/*
- * Copyright 2019 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- *      http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-package androidx.compose.compiler.plugins.kotlin
-
-import org.jetbrains.kotlin.backend.common.phaser.PhaseConfig
-import org.jetbrains.kotlin.backend.jvm.JvmIrCodegenFactory
-import org.jetbrains.kotlin.backend.jvm.jvmPhases
-import org.jetbrains.kotlin.cli.common.CLIConfigurationKeys
-import org.jetbrains.kotlin.cli.jvm.compiler.KotlinCoreEnvironment
-import org.jetbrains.kotlin.codegen.ClassBuilderFactories
-import org.jetbrains.kotlin.codegen.KotlinCodegenFacade
-import org.jetbrains.kotlin.codegen.state.GenerationState
-import org.jetbrains.kotlin.psi.KtFile
-import org.jetbrains.kotlin.resolve.AnalyzingUtils
-
-object GenerationUtils {
-    fun compileFiles(
-        environment: KotlinCoreEnvironment,
-        files: List<KtFile>,
-    ): GenerationState {
-        val analysisResult = JvmResolveUtil.analyzeAndCheckForErrors(environment, files)
-        analysisResult.throwIfError()
-
-        val state = GenerationState.Builder(
-            environment.project,
-            ClassBuilderFactories.TEST,
-            analysisResult.moduleDescriptor,
-            analysisResult.bindingContext,
-            files,
-            environment.configuration
-        ).codegenFactory(
-            JvmIrCodegenFactory(
-                environment.configuration,
-                environment.configuration.get(CLIConfigurationKeys.PHASE_CONFIG)
-                    ?: PhaseConfig(jvmPhases)
-            )
-        ).isIrBackend(true).build()
-
-        KotlinCodegenFacade.compileCorrectFiles(state)
-
-        // For JVM-specific errors
-        try {
-            AnalyzingUtils.throwExceptionOnErrors(state.collectedExtraJvmDiagnostics)
-        } catch (e: Throwable) {
-            throw TestsCompilerError(e)
-        }
-
-        return state
-    }
-}
diff --git a/compose/compiler/compiler-hosted/integration-tests/src/test/java/androidx/compose/compiler/plugins/kotlin/JvmResolveUtil.kt b/compose/compiler/compiler-hosted/integration-tests/src/test/java/androidx/compose/compiler/plugins/kotlin/JvmResolveUtil.kt
deleted file mode 100644
index 629bfca..0000000
--- a/compose/compiler/compiler-hosted/integration-tests/src/test/java/androidx/compose/compiler/plugins/kotlin/JvmResolveUtil.kt
+++ /dev/null
@@ -1,56 +0,0 @@
-/*
- * Copyright 2019 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- *      http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-package androidx.compose.compiler.plugins.kotlin
-
-import org.jetbrains.kotlin.analyzer.AnalysisResult
-import org.jetbrains.kotlin.cli.jvm.compiler.KotlinCoreEnvironment
-import org.jetbrains.kotlin.cli.jvm.compiler.NoScopeRecordCliBindingTrace
-import org.jetbrains.kotlin.cli.jvm.compiler.TopDownAnalyzerFacadeForJVM
-import org.jetbrains.kotlin.psi.KtFile
-import org.jetbrains.kotlin.resolve.AnalyzingUtils
-
-object JvmResolveUtil {
-    fun analyzeAndCheckForErrors(
-        environment: KotlinCoreEnvironment,
-        files: Collection<KtFile>
-    ): AnalysisResult {
-        for (file in files) {
-            try {
-                AnalyzingUtils.checkForSyntacticErrors(file)
-            } catch (e: Exception) {
-                throw TestsCompilerError(e)
-            }
-        }
-
-        return analyze(environment, files).apply {
-            try {
-                AnalyzingUtils.throwExceptionOnErrors(bindingContext)
-            } catch (e: Exception) {
-                throw TestsCompilerError(e)
-            }
-        }
-    }
-
-    fun analyze(environment: KotlinCoreEnvironment, files: Collection<KtFile>): AnalysisResult =
-        TopDownAnalyzerFacadeForJVM.analyzeFilesWithJavaIntegration(
-            environment.project,
-            files,
-            NoScopeRecordCliBindingTrace(),
-            environment.configuration,
-            environment::createPackagePartProvider
-        )
-}
diff --git a/compose/compiler/compiler-hosted/integration-tests/src/test/java/androidx/compose/compiler/plugins/kotlin/KtxCrossModuleTests.kt b/compose/compiler/compiler-hosted/integration-tests/src/test/java/androidx/compose/compiler/plugins/kotlin/KtxCrossModuleTests.kt
index da19e2d..d310b12 100644
--- a/compose/compiler/compiler-hosted/integration-tests/src/test/java/androidx/compose/compiler/plugins/kotlin/KtxCrossModuleTests.kt
+++ b/compose/compiler/compiler-hosted/integration-tests/src/test/java/androidx/compose/compiler/plugins/kotlin/KtxCrossModuleTests.kt
@@ -22,8 +22,11 @@
 import java.io.File
 import java.net.URLClassLoader
 import org.jetbrains.kotlin.backend.common.output.OutputFile
+import org.junit.Assert.assertEquals
 import org.junit.Ignore
+import org.junit.Rule
 import org.junit.Test
+import org.junit.rules.TemporaryFolder
 import org.junit.runner.RunWith
 import org.robolectric.RobolectricTestRunner
 import org.robolectric.annotation.Config
@@ -35,9 +38,8 @@
     maxSdk = 23
 )
 class KtxCrossModuleTests : AbstractCodegenTest() {
-
     @Test
-    fun testInlineFunctionDefaultArgument(): Unit = ensureSetup {
+    fun testInlineFunctionDefaultArgument() {
         compile(
             mapOf(
                 "library module" to mapOf(
@@ -72,7 +74,7 @@
     }
 
     @Test
-    fun testInlineFunctionDefaultArgument2(): Unit = ensureSetup {
+    fun testInlineFunctionDefaultArgument2() {
         compile(
             mapOf(
                 "library module" to mapOf(
@@ -105,7 +107,7 @@
     }
 
     @Test
-    fun testAccessibilityBridgeGeneration(): Unit = ensureSetup {
+    fun testAccessibilityBridgeGeneration() {
         compile(
             mapOf(
                 "library module" to mapOf(
@@ -152,7 +154,7 @@
     }
 
     @Test
-    fun testInlineClassCrossModule(): Unit = ensureSetup {
+    fun testInlineClassCrossModule() {
         compile(
             mapOf(
                 "library module" to mapOf(
@@ -188,7 +190,7 @@
     }
 
     @Test // see: b/255983530
-    fun testNonComposableWithComposableReturnTypeCrossModule(): Unit = ensureSetup {
+    fun testNonComposableWithComposableReturnTypeCrossModule() {
         compile(
             mapOf(
                 "library module" to mapOf(
@@ -233,7 +235,7 @@
     }
 
     @Test // see: b/255983530
-    fun testNonComposableWithNestedComposableReturnTypeCrossModule(): Unit = ensureSetup {
+    fun testNonComposableWithNestedComposableReturnTypeCrossModule() {
         compile(
             mapOf(
                 "library module" to mapOf(
@@ -270,7 +272,7 @@
     }
 
     @Test
-    fun testInlineClassOverloading(): Unit = ensureSetup {
+    fun testInlineClassOverloading() {
         compile(
             mapOf(
                 "library module" to mapOf(
@@ -319,7 +321,7 @@
     }
 
     @Test
-    fun testFunInterfaceWithInlineClass(): Unit = ensureSetup {
+    fun testFunInterfaceWithInlineClass() {
         compile(
             mapOf(
                 "library module" to mapOf(
@@ -350,7 +352,7 @@
     }
 
     @Test
-    fun testParentNotInitializedBug(): Unit = ensureSetup {
+    fun testParentNotInitializedBug() {
         compile(
             mapOf(
                 "library module" to mapOf(
@@ -386,7 +388,7 @@
     }
 
     @Test
-    fun testConstCrossModule(): Unit = ensureSetup {
+    fun testConstCrossModule() {
         compile(
             mapOf(
                 "library module" to mapOf(
@@ -415,7 +417,7 @@
     }
 
     @Test
-    fun testNonCrossinlineComposable(): Unit = ensureSetup {
+    fun testNonCrossinlineComposable() {
         compile(
             mapOf(
                 "library module" to mapOf(
@@ -447,7 +449,7 @@
     }
 
     @Test
-    fun testNonCrossinlineComposableNoGenerics(): Unit = ensureSetup {
+    fun testNonCrossinlineComposableNoGenerics() {
         compile(
             mapOf(
                 "library module" to mapOf(
@@ -481,7 +483,7 @@
     }
 
     @Test
-    fun testRemappedTypes(): Unit = ensureSetup {
+    fun testRemappedTypes() {
         compile(
             mapOf(
                 "library module" to mapOf(
@@ -517,7 +519,7 @@
     }
 
     @Test
-    fun testInlineIssue(): Unit = ensureSetup {
+    fun testInlineIssue() {
         compile(
             mapOf(
                 "library module" to mapOf(
@@ -546,7 +548,7 @@
     }
 
     @Test
-    fun testInlineComposableProperty(): Unit = ensureSetup {
+    fun testInlineComposableProperty() {
         compile(
             mapOf(
                 "library module" to mapOf(
@@ -579,7 +581,7 @@
     }
 
     @Test
-    fun testNestedInlineIssue(): Unit = ensureSetup {
+    fun testNestedInlineIssue() {
         compile(
             mapOf(
                 "library module" to mapOf(
@@ -616,7 +618,7 @@
     }
 
     @Test
-    fun testComposerIntrinsicInline(): Unit = ensureSetup {
+    fun testComposerIntrinsicInline() {
         compile(
             mapOf(
                 "library module" to mapOf(
@@ -657,7 +659,7 @@
     }
 
     @Test
-    fun testComposableOrderIssue(): Unit = ensureSetup {
+    fun testComposableOrderIssue() {
         compile(
             mapOf(
                 "library module" to mapOf(
@@ -696,7 +698,7 @@
     }
 
     @Test
-    fun testSimpleXModuleCall(): Unit = ensureSetup {
+    fun testSimpleXModuleCall() {
         compile(
             mapOf(
                 "library module" to mapOf(
@@ -727,7 +729,7 @@
     }
 
     @Test
-    fun testJvmFieldIssue(): Unit = ensureSetup {
+    fun testJvmFieldIssue() {
         compile(
             mapOf(
                 "library module" to mapOf(
@@ -756,7 +758,7 @@
     }
 
     @Test
-    fun testInstanceXModuleCall(): Unit = ensureSetup {
+    fun testInstanceXModuleCall() {
         compile(
             mapOf(
                 "library module" to mapOf(
@@ -787,7 +789,7 @@
     }
 
     @Test
-    fun testXModuleProperty(): Unit = ensureSetup {
+    fun testXModuleProperty() {
         compile(
             mapOf(
                 "library module" to mapOf(
@@ -815,7 +817,7 @@
     }
 
     @Test
-    fun testXModuleInterface(): Unit = ensureSetup {
+    fun testXModuleInterface() {
         compile(
             mapOf(
                 "library module" to mapOf(
@@ -849,7 +851,7 @@
     }
 
     @Test
-    fun testXModuleComposableProperty(): Unit = ensureSetup {
+    fun testXModuleComposableProperty() {
         compile(
             mapOf(
                 "library module" to mapOf(
@@ -878,7 +880,7 @@
     }
 
     @Test
-    fun testXModuleCtorComposableParam(): Unit = ensureSetup {
+    fun testXModuleCtorComposableParam() {
         compile(
             mapOf(
                 "library module" to mapOf(
@@ -906,7 +908,7 @@
 
     @Ignore("b/171801506")
     @Test
-    fun testCrossModule_SimpleComposition(): Unit = ensureSetup {
+    fun testCrossModule_SimpleComposition() {
         val tvId = 29
 
         compose(
@@ -973,7 +975,7 @@
      * Test for b/169071070
      */
     @Test
-    fun testCrossModule_ComposableInterfaceFunctionWithInlineClasses(): Unit = ensureSetup {
+    fun testCrossModule_ComposableInterfaceFunctionWithInlineClasses() {
         compile(
             mapOf(
                 "library module" to mapOf(
@@ -1009,7 +1011,7 @@
     }
 
     @Test
-    fun testAnnotationInferenceAcrossModules() = ensureSetup {
+    fun testAnnotationInferenceAcrossModules() {
         compile(
             mapOf(
                 "Base" to mapOf(
@@ -1068,7 +1070,7 @@
      * Test for b/221280935
      */
     @Test
-    fun testOverriddenSymbolParentsInDefaultParameters() = ensureSetup {
+    fun testOverriddenSymbolParentsInDefaultParameters() {
         compile(
             mapOf(
                 "Base" to mapOf(
@@ -1103,30 +1105,23 @@
         dumpClasses: Boolean = false,
         validate: ((String) -> Unit)? = null
     ): List<OutputFile> {
-        val libraryClasses = (
-            modules.filter { it.key != "Main" }.map {
-                // Setup for compile
-                this.classFileFactory = null
-                this.myEnvironment = null
-                setUp()
-
-                classLoader(it.value, dumpClasses).allGeneratedFiles.also { outputFiles ->
-                    // Write the files to the class directory so they can be used by the next module
-                    // and the application
-                    outputFiles.writeToDir(classesDirectory)
-                }
-            } + emptyList()
-            ).reduce { acc, mutableList -> acc + mutableList }
-
-        // Setup for compile
-        this.classFileFactory = null
-        this.myEnvironment = null
-        setUp()
+        val libraryClasses = modules.filter { it.key != "Main" }.flatMap {
+            classLoader(
+                it.value,
+                listOf(classesDirectory.root),
+                dumpClasses
+            ).allGeneratedFiles.also { outputFiles ->
+                // Write the files to the class directory so they can be used by the next module
+                // and the application
+                outputFiles.writeToDir(classesDirectory.root)
+            }
+        }
 
         // compile the next one
         val appClasses = classLoader(
             modules["Main"]
                 ?: error("No Main module specified"),
+            listOf(classesDirectory.root),
             dumpClasses
         ).allGeneratedFiles
 
@@ -1177,12 +1172,9 @@
         }
     }
 
-    private var testLocalUnique = 0
-    private var classesDirectory = tmpDir(
-        "kotlin-${testLocalUnique++}-classes"
-    )
-
-    override val additionalPaths: List<File> = listOf(classesDirectory)
+    @JvmField
+    @Rule
+    val classesDirectory = TemporaryFolder()
 }
 
 fun OutputFile.writeToDir(directory: File) =
diff --git a/compose/compiler/compiler-hosted/integration-tests/src/test/java/androidx/compose/compiler/plugins/kotlin/KtxTransformationTest.kt b/compose/compiler/compiler-hosted/integration-tests/src/test/java/androidx/compose/compiler/plugins/kotlin/KtxTransformationTest.kt
index a7f94da..4608a38 100644
--- a/compose/compiler/compiler-hosted/integration-tests/src/test/java/androidx/compose/compiler/plugins/kotlin/KtxTransformationTest.kt
+++ b/compose/compiler/compiler-hosted/integration-tests/src/test/java/androidx/compose/compiler/plugins/kotlin/KtxTransformationTest.kt
@@ -16,10 +16,12 @@
 
 package androidx.compose.compiler.plugins.kotlin
 
-class KtxTransformationTest : AbstractCodegenTest() {
+import org.junit.Test
 
+class KtxTransformationTest : AbstractCodegenTest() {
 //    b/179279455
-//    fun testObserveLowering() = ensureSetup {
+//    @Test
+//    fun testObserveLowering() {
 //        testCompileWithViewStubs(
 //            """
 //            import androidx.compose.runtime.MutableState
@@ -42,7 +44,8 @@
 //        )
 //    }
 
-    fun testEmptyComposeFunction() = ensureSetup {
+    @Test
+    fun testEmptyComposeFunction() {
         testCompile(
             """
         import androidx.compose.runtime.*
@@ -56,7 +59,8 @@
     }
 
 //    "b/179279455"
-//    fun testSingleViewCompose() = ensureSetup {
+//    @Test
+//    fun testSingleViewCompose() {
 //        testCompileWithViewStubs(
 //            """
 //        class Foo {
@@ -70,7 +74,8 @@
 //    }
 
 //    "b/179279455"
-//    fun testMultipleRootViewCompose() = ensureSetup {
+//    @Test
+//    fun testMultipleRootViewCompose() {
 //        testCompileWithViewStubs(
 //            """
 //        class Foo {
@@ -86,7 +91,8 @@
 //    }
 
 //    "b/179279455"
-//    fun testNestedViewCompose() = ensureSetup {
+//    @Test
+//    fun testNestedViewCompose() {
 //        testCompileWithViewStubs(
 //            """
 //        class Foo {
@@ -105,7 +111,8 @@
 //        )
 //    }
 
-    fun testSingleComposite() = ensureSetup {
+    @Test
+    fun testSingleComposite() {
         testCompile(
             """
          import androidx.compose.runtime.*
@@ -123,7 +130,8 @@
         )
     }
 
-    fun testMultipleRootComposite() = ensureSetup {
+    @Test
+    fun testMultipleRootComposite() {
         testCompile(
             """
          import androidx.compose.runtime.*
@@ -144,7 +152,8 @@
     }
 
 //    "b/179279455"
-//    fun testViewAndComposites() = ensureSetup {
+//    @Test
+//    fun testViewAndComposites() {
 //        testCompileWithViewStubs(
 //            """
 //        @Composable
@@ -162,7 +171,8 @@
 //        )
 //    }
 
-    fun testForEach() = ensureSetup {
+    @Test
+    fun testForEach() {
         testCompile(
             """
          import androidx.compose.runtime.*
@@ -182,7 +192,8 @@
         )
     }
 
-    fun testForLoop() = ensureSetup {
+    @Test
+    fun testForLoop() {
         testCompile(
             """
          import androidx.compose.runtime.*
@@ -202,7 +213,8 @@
         )
     }
 
-    fun testEarlyReturns() = ensureSetup {
+    @Test
+    fun testEarlyReturns() {
         testCompile(
             """
          import androidx.compose.runtime.*
@@ -223,7 +235,8 @@
         )
     }
 
-    fun testConditionalRendering() = ensureSetup {
+    @Test
+    fun testConditionalRendering() {
         testCompile(
             """
          import androidx.compose.runtime.*
@@ -250,7 +263,8 @@
     }
 
 //    "b/179279455"
-//    fun testChildrenWithTypedParameters() = ensureSetup {
+//    @Test
+//    fun testChildrenWithTypedParameters() {
 //        testCompileWithViewStubs(
 //            """
 //        @Composable fun HelperComponent(
@@ -275,7 +289,8 @@
 //    }
 
 //    "b/179279455"
-//    fun testChildrenCaptureVariables() = ensureSetup {
+//    @Test
+//    fun testChildrenCaptureVariables() {
 //        testCompileWithViewStubs(
 //            """
 //        @Composable fun HelperComponent(content: @Composable () -> Unit) {
@@ -295,7 +310,8 @@
 //        )
 //    }
 
-    fun testChildrenDeepCaptureVariables() = ensureSetup {
+    @Test
+    fun testChildrenDeepCaptureVariables() {
         testCompile(
             """
         import android.widget.*
@@ -325,7 +341,8 @@
         )
     }
 
-    fun testChildrenDeepCaptureVariablesWithParameters() = ensureSetup {
+    @Test
+    fun testChildrenDeepCaptureVariablesWithParameters() {
         testCompile(
             """
         import android.widget.*
@@ -356,7 +373,8 @@
     }
 
 //    "b/179279455"
-//    fun testChildrenOfNativeView() = ensureSetup {
+//    @Test
+//    fun testChildrenOfNativeView() {
 //        testCompileWithViewStubs(
 //            """
 //        class MainComponent {
@@ -373,7 +391,8 @@
 //    }
 
 //    "b/179279455"
-//    fun testIrSpecial() = ensureSetup {
+//    @Test
+//    fun testIrSpecial() {
 //        testCompileWithViewStubs(
 //            """
 //        @Composable fun HelperComponent(content: @Composable () -> Unit) {}
@@ -394,7 +413,8 @@
 //        )
 //    }
 
-    fun testGenericsInnerClass() = ensureSetup {
+    @Test
+    fun testGenericsInnerClass() {
         testCompile(
             """
         import androidx.compose.runtime.*
@@ -415,7 +435,8 @@
         )
     }
 
-    fun testXGenericConstructorParams() = ensureSetup {
+    @Test
+    fun testXGenericConstructorParams() {
         testCompile(
             """
         import androidx.compose.runtime.*
@@ -444,7 +465,8 @@
         )
     }
 
-    fun testSimpleNoArgsComponent() = ensureSetup {
+    @Test
+    fun testSimpleNoArgsComponent() {
         testCompile(
             """
         import androidx.compose.runtime.*
@@ -460,7 +482,8 @@
         )
     }
 
-    fun testDotQualifiedObjectToClass() = ensureSetup {
+    @Test
+    fun testDotQualifiedObjectToClass() {
         testCompile(
             """
         import androidx.compose.runtime.*
@@ -478,7 +501,8 @@
         )
     }
 
-    fun testLocalLambda() = ensureSetup {
+    @Test
+    fun testLocalLambda() {
         testCompile(
             """
         import androidx.compose.runtime.*
@@ -495,7 +519,8 @@
         )
     }
 
-    fun testPropertyLambda() = ensureSetup {
+    @Test
+    fun testPropertyLambda() {
         testCompile(
             """
         import androidx.compose.runtime.*
@@ -510,7 +535,8 @@
         )
     }
 
-    fun testLambdaWithArgs() = ensureSetup {
+    @Test
+    fun testLambdaWithArgs() {
         testCompile(
             """
         import androidx.compose.runtime.*
@@ -525,7 +551,8 @@
         )
     }
 
-    fun testLocalMethod() = ensureSetup {
+    @Test
+    fun testLocalMethod() {
         testCompile(
             """
         import androidx.compose.runtime.*
@@ -542,7 +569,8 @@
         )
     }
 
-    fun testSimpleLambdaChildren() = ensureSetup {
+    @Test
+    fun testSimpleLambdaChildren() {
         testCompile(
             """
         import androidx.compose.runtime.*
@@ -563,7 +591,8 @@
         )
     }
 
-    fun testFunctionComponentsWithChildrenSimple() = ensureSetup {
+    @Test
+    fun testFunctionComponentsWithChildrenSimple() {
         testCompile(
             """
         import androidx.compose.runtime.*
@@ -581,7 +610,8 @@
         )
     }
 
-    fun testFunctionComponentWithChildrenOneArg() = ensureSetup {
+    @Test
+    fun testFunctionComponentWithChildrenOneArg() {
         testCompile(
             """
         import androidx.compose.runtime.*
@@ -599,7 +629,8 @@
         )
     }
 
-    fun testKtxLambdaInForLoop() = ensureSetup {
+    @Test
+    fun testKtxLambdaInForLoop() {
         testCompile(
             """
         import androidx.compose.runtime.*
@@ -618,7 +649,8 @@
     }
 
 //    "b/179279455"
-//    fun testKtxLambdaInIfElse() = ensureSetup {
+//    @Test
+//    fun testKtxLambdaInIfElse() {
 //        testCompileWithViewStubs(
 //            """
 //        @Composable
@@ -636,7 +668,8 @@
 //        )
 //    }
 
-    fun testKtxVariableTagsProperlyCapturedAcrossKtxLambdas() = ensureSetup {
+    @Test
+    fun testKtxVariableTagsProperlyCapturedAcrossKtxLambdas() {
         testCompile(
             """
         import androidx.compose.runtime.*
@@ -663,7 +696,8 @@
         )
     }
 
-    fun testInvocableObject() = ensureSetup {
+    @Test
+    fun testInvocableObject() {
         testCompile(
             """
         import androidx.compose.runtime.*
diff --git a/compose/compiler/compiler-hosted/integration-tests/src/test/java/androidx/compose/compiler/plugins/kotlin/LambdaMemoizationTests.kt b/compose/compiler/compiler-hosted/integration-tests/src/test/java/androidx/compose/compiler/plugins/kotlin/LambdaMemoizationTests.kt
index 8905d89..2b9d52a 100644
--- a/compose/compiler/compiler-hosted/integration-tests/src/test/java/androidx/compose/compiler/plugins/kotlin/LambdaMemoizationTests.kt
+++ b/compose/compiler/compiler-hosted/integration-tests/src/test/java/androidx/compose/compiler/plugins/kotlin/LambdaMemoizationTests.kt
@@ -33,7 +33,6 @@
     maxSdk = 23
 )
 class LambdaMemoizationTests : AbstractLoweringTests() {
-
     @Test
     @Ignore("b/179279455")
     fun nonCapturingEventLambda() = skipping(
@@ -937,92 +936,91 @@
         """
     )
 
-    private fun skipping(@Language("kotlin") text: String, dumpClasses: Boolean = false) =
-        ensureSetup {
-            compose(
-                """
-                var avoidedWorkCount = 0
-                var repeatedWorkCount = 0
-                var expectedAvoidedWorkCount = 0
-                var expectedRepeatedWorkCount = 0
+    private fun skipping(@Language("kotlin") text: String, dumpClasses: Boolean = false) {
+        compose(
+            """
+            var avoidedWorkCount = 0
+            var repeatedWorkCount = 0
+            var expectedAvoidedWorkCount = 0
+            var expectedRepeatedWorkCount = 0
 
-                fun workToBeAvoided(msg: String = "") {
-                   avoidedWorkCount++
-                   // println("Work to be avoided ${'$'}avoidedWorkCount ${'$'}msg")
-                }
-                fun workToBeRepeated(msg: String = "") {
-                   repeatedWorkCount++
-                   // println("Work to be repeated ${'$'}repeatedWorkCount ${'$'}msg")
-                }
-
-                $text
-
-                @Composable
-                fun Display(text: String) {}
-
-                fun validateModel(text: String) {
-                  require(text == "Iteration ${'$'}iterations")
-                }
-
-                @Composable
-                fun ValidateModel(text: String) {
-                  validateModel(text)
-                }
-
-                @Composable
-                fun TestHost() {
-                   // println("START: Iteration - ${'$'}iterations")
-                   val scope = currentRecomposeScope
-                   emitView(::Button) {
-                     it.id=42
-                     it.setOnClickListener(View.OnClickListener { scope.invalidate() })
-                   }
-                   Example("Iteration ${'$'}iterations")
-                   // println("END  : Iteration - ${'$'}iterations")
-                   validate()
-                }
-
-                var iterations = 0
-
-                fun validate() {
-                  if (iterations++ == 0) {
-                    expectedAvoidedWorkCount = avoidedWorkCount
-                    expectedRepeatedWorkCount = repeatedWorkCount
-                    repeatedWorkCount = 0
-                  } else {
-                    if (expectedAvoidedWorkCount != avoidedWorkCount) {
-                      println("Executed avoided work")
-                    }
-                    require(expectedAvoidedWorkCount == avoidedWorkCount) {
-                      "Executed avoided work unexpectedly, expected " +
-                      "${'$'}expectedAvoidedWorkCount" +
-                      ", received ${'$'}avoidedWorkCount"
-                    }
-                    if (expectedRepeatedWorkCount != repeatedWorkCount) {
-                      println("Will throw Executed more work")
-                    }
-                    require(expectedRepeatedWorkCount == repeatedWorkCount) {
-                      "Expected more repeated work, expected ${'$'}expectedRepeatedWorkCount" +
-                      ", received ${'$'}repeatedWorkCount"
-                    }
-                    repeatedWorkCount = 0
-                  }
-                }
-
-            """,
-                """
-                TestHost()
-            """,
-                dumpClasses = dumpClasses
-            ).then { activity ->
-                val button = activity.findViewById(42) as Button
-                button.performClick()
-            }.then { activity ->
-                val button = activity.findViewById(42) as Button
-                button.performClick()
-            }.then {
-                // Wait for test to complete
-                shadowOf(getMainLooper()).idle()
+            fun workToBeAvoided(msg: String = "") {
+               avoidedWorkCount++
+               // println("Work to be avoided ${'$'}avoidedWorkCount ${'$'}msg")
             }
+            fun workToBeRepeated(msg: String = "") {
+               repeatedWorkCount++
+               // println("Work to be repeated ${'$'}repeatedWorkCount ${'$'}msg")
+            }
+
+            $text
+
+            @Composable
+            fun Display(text: String) {}
+
+            fun validateModel(text: String) {
+              require(text == "Iteration ${'$'}iterations")
+            }
+
+            @Composable
+            fun ValidateModel(text: String) {
+              validateModel(text)
+            }
+
+            @Composable
+            fun TestHost() {
+               // println("START: Iteration - ${'$'}iterations")
+               val scope = currentRecomposeScope
+               emitView(::Button) {
+                 it.id=42
+                 it.setOnClickListener(View.OnClickListener { scope.invalidate() })
+               }
+               Example("Iteration ${'$'}iterations")
+               // println("END  : Iteration - ${'$'}iterations")
+               validate()
+            }
+
+            var iterations = 0
+
+            fun validate() {
+              if (iterations++ == 0) {
+                expectedAvoidedWorkCount = avoidedWorkCount
+                expectedRepeatedWorkCount = repeatedWorkCount
+                repeatedWorkCount = 0
+              } else {
+                if (expectedAvoidedWorkCount != avoidedWorkCount) {
+                  println("Executed avoided work")
+                }
+                require(expectedAvoidedWorkCount == avoidedWorkCount) {
+                  "Executed avoided work unexpectedly, expected " +
+                  "${'$'}expectedAvoidedWorkCount" +
+                  ", received ${'$'}avoidedWorkCount"
+                }
+                if (expectedRepeatedWorkCount != repeatedWorkCount) {
+                  println("Will throw Executed more work")
+                }
+                require(expectedRepeatedWorkCount == repeatedWorkCount) {
+                  "Expected more repeated work, expected ${'$'}expectedRepeatedWorkCount" +
+                  ", received ${'$'}repeatedWorkCount"
+                }
+                repeatedWorkCount = 0
+              }
+            }
+
+        """,
+            """
+            TestHost()
+        """,
+            dumpClasses = dumpClasses
+        ).then { activity ->
+            val button = activity.findViewById(42) as Button
+            button.performClick()
+        }.then { activity ->
+            val button = activity.findViewById(42) as Button
+            button.performClick()
+        }.then {
+            // Wait for test to complete
+            shadowOf(getMainLooper()).idle()
         }
+    }
 }
diff --git a/compose/compiler/compiler-hosted/integration-tests/src/test/java/androidx/compose/compiler/plugins/kotlin/LambdaMemoizationTransformTests.kt b/compose/compiler/compiler-hosted/integration-tests/src/test/java/androidx/compose/compiler/plugins/kotlin/LambdaMemoizationTransformTests.kt
index 38aec5c..e8d6037 100644
--- a/compose/compiler/compiler-hosted/integration-tests/src/test/java/androidx/compose/compiler/plugins/kotlin/LambdaMemoizationTransformTests.kt
+++ b/compose/compiler/compiler-hosted/integration-tests/src/test/java/androidx/compose/compiler/plugins/kotlin/LambdaMemoizationTransformTests.kt
@@ -20,7 +20,7 @@
 
 class LambdaMemoizationTransformTests : AbstractIrTransformTest() {
     @Test
-    fun testCapturedThisFromFieldInitializer(): Unit = verifyComposeIrTransform(
+    fun testCapturedThisFromFieldInitializer() = verifyComposeIrTransform(
         """
             import androidx.compose.runtime.Composable
 
@@ -57,7 +57,7 @@
     )
 
     @Test
-    fun testLocalInALocal(): Unit = verifyComposeIrTransform(
+    fun testLocalInALocal() = verifyComposeIrTransform(
         """
             import androidx.compose.runtime.Composable
 
@@ -147,7 +147,7 @@
 
     // Fixes b/201252574
     @Test
-    fun testLocalFunCaptures(): Unit = verifyComposeIrTransform(
+    fun testLocalFunCaptures() = verifyComposeIrTransform(
         """
             import androidx.compose.runtime.NonRestartableComposable
             import androidx.compose.runtime.Composable
@@ -193,7 +193,7 @@
     )
 
     @Test
-    fun testLocalClassCaptures1(): Unit = verifyComposeIrTransform(
+    fun testLocalClassCaptures1() = verifyComposeIrTransform(
         """
             import androidx.compose.runtime.NonRestartableComposable
             import androidx.compose.runtime.Composable
@@ -241,7 +241,7 @@
     )
 
     @Test
-    fun testLocalClassCaptures2(): Unit = verifyComposeIrTransform(
+    fun testLocalClassCaptures2() = verifyComposeIrTransform(
         """
             import androidx.compose.runtime.Composable
             import androidx.compose.runtime.NonRestartableComposable
@@ -283,7 +283,7 @@
     )
 
     @Test
-    fun testLocalFunCaptures3(): Unit = verifyComposeIrTransform(
+    fun testLocalFunCaptures3() = verifyComposeIrTransform(
         """
             import androidx.compose.runtime.Composable
 
@@ -351,7 +351,7 @@
     )
 
     @Test
-    fun testStateDelegateCapture(): Unit = verifyComposeIrTransform(
+    fun testStateDelegateCapture() = verifyComposeIrTransform(
         """
             import androidx.compose.runtime.Composable
             import androidx.compose.runtime.mutableStateOf
@@ -407,7 +407,7 @@
     )
 
     @Test
-    fun testTopLevelComposableLambdaProperties(): Unit = verifyComposeIrTransform(
+    fun testTopLevelComposableLambdaProperties() = verifyComposeIrTransform(
         """
             import androidx.compose.runtime.Composable
 
@@ -453,7 +453,7 @@
     )
 
     @Test
-    fun testLocalVariableComposableLambdas(): Unit = verifyComposeIrTransform(
+    fun testLocalVariableComposableLambdas() = verifyComposeIrTransform(
         """
             import androidx.compose.runtime.Composable
 
@@ -525,7 +525,7 @@
     )
 
     @Test
-    fun testParameterComposableLambdas(): Unit = verifyComposeIrTransform(
+    fun testParameterComposableLambdas() = verifyComposeIrTransform(
         """
             import androidx.compose.runtime.Composable
 
@@ -577,7 +577,7 @@
     )
 
     @Test // Regression test for b/180168881
-    fun testFunctionReferenceWithinInferredComposableLambda(): Unit = verifyComposeIrTransform(
+    fun testFunctionReferenceWithinInferredComposableLambda() = verifyComposeIrTransform(
         """
             import androidx.compose.runtime.Composable
 
@@ -610,7 +610,7 @@
     )
 
     @Test
-    fun testFunctionReferenceNonComposableMemoization(): Unit = verifyComposeIrTransform(
+    fun testFunctionReferenceNonComposableMemoization() = verifyComposeIrTransform(
         """
             import androidx.compose.runtime.Composable
             @Composable fun Example(x: Int) {
@@ -656,7 +656,7 @@
     )
 
     @Test // regression of b/162575428
-    fun testComposableInAFunctionParameter(): Unit = verifyComposeIrTransform(
+    fun testComposableInAFunctionParameter() = verifyComposeIrTransform(
         """
             import androidx.compose.runtime.Composable
 
@@ -725,7 +725,7 @@
     )
 
     @Test
-    fun testComposabableLambdaInLocalDeclaration(): Unit = verifyComposeIrTransform(
+    fun testComposabableLambdaInLocalDeclaration() = verifyComposeIrTransform(
         """
             import androidx.compose.runtime.Composable
 
diff --git a/compose/compiler/compiler-hosted/integration-tests/src/test/java/androidx/compose/compiler/plugins/kotlin/LiveLiteralCodegenTests.kt b/compose/compiler/compiler-hosted/integration-tests/src/test/java/androidx/compose/compiler/plugins/kotlin/LiveLiteralCodegenTests.kt
index 3d406f2..1083583 100644
--- a/compose/compiler/compiler-hosted/integration-tests/src/test/java/androidx/compose/compiler/plugins/kotlin/LiveLiteralCodegenTests.kt
+++ b/compose/compiler/compiler-hosted/integration-tests/src/test/java/androidx/compose/compiler/plugins/kotlin/LiveLiteralCodegenTests.kt
@@ -20,6 +20,7 @@
 import androidx.compose.runtime.InternalComposeApi
 import androidx.compose.runtime.internal.updateLiveLiteralValue
 import org.jetbrains.kotlin.config.CompilerConfiguration
+import org.junit.Assert.assertEquals
 import org.junit.Ignore
 import org.junit.Test
 import org.junit.runner.RunWith
@@ -33,15 +34,13 @@
     maxSdk = 23
 )
 class LiveLiteralCodegenTests : AbstractLoweringTests() {
-
-    override fun updateConfiguration(configuration: CompilerConfiguration) {
-        super.updateConfiguration(configuration)
-        configuration.put(ComposeConfiguration.LIVE_LITERALS_ENABLED_KEY, true)
+    override fun CompilerConfiguration.updateConfiguration() {
+        put(ComposeConfiguration.LIVE_LITERALS_ENABLED_KEY, true)
     }
 
     @Ignore("Live literals are currently disabled by default")
     @Test
-    fun testBasicFunctionality(): Unit = ensureSetup {
+    fun testBasicFunctionality() {
         compose(
             """
             @Composable
@@ -63,7 +62,7 @@
 
     @Ignore("Live literals are currently disabled by default")
     @Test
-    fun testObjectFieldsLoweredToStaticFields(): Unit = ensureSetup {
+    fun testObjectFieldsLoweredToStaticFields() {
         validateBytecode(
             """
             fun Test(): Int {
diff --git a/compose/compiler/compiler-hosted/integration-tests/src/test/java/androidx/compose/compiler/plugins/kotlin/LiveLiteralTransformTests.kt b/compose/compiler/compiler-hosted/integration-tests/src/test/java/androidx/compose/compiler/plugins/kotlin/LiveLiteralTransformTests.kt
index 792f46f..d691fee 100644
--- a/compose/compiler/compiler-hosted/integration-tests/src/test/java/androidx/compose/compiler/plugins/kotlin/LiveLiteralTransformTests.kt
+++ b/compose/compiler/compiler-hosted/integration-tests/src/test/java/androidx/compose/compiler/plugins/kotlin/LiveLiteralTransformTests.kt
@@ -16,12 +16,15 @@
 
 package androidx.compose.compiler.plugins.kotlin
 
+import org.jetbrains.kotlin.config.CompilerConfiguration
 import org.junit.Test
 
 class LiveLiteralTransformTests : AbstractLiveLiteralTransformTests() {
-    override val liveLiteralsEnabled: Boolean
-        get() = true
+    override fun CompilerConfiguration.updateConfiguration() {
+        put(ComposeConfiguration.LIVE_LITERALS_ENABLED_KEY, true)
+    }
 
+    @Test
     fun testSiblingCallArgs() = assertNoDuplicateKeys(
         """
         fun Test() {
@@ -31,6 +34,7 @@
         """
     )
 
+    @Test
     fun testFunctionCallWithConstArg() = assertKeys(
         "Int%arg-0%call-print%fun-Test",
         "Int%arg-0%call-print-1%fun-Test"
@@ -43,6 +47,7 @@
         """
     }
 
+    @Test
     fun testDispatchReceiver() = assertKeys(
         "Int%%this%call-toString%arg-0%call-print%fun-Test",
         "Int%arg-0%call-print-1%fun-Test"
@@ -55,6 +60,7 @@
         """
     }
 
+    @Test
     fun testInsidePropertyGetter() = assertKeys(
         "Int%fun-%get-foo%%get%val-foo"
     ) {
@@ -64,12 +70,14 @@
     }
 
     // NOTE(lmr): For static initializer expressions we can/should do more.
+    @Test
     fun testInsidePropertyInitializer() = assertKeys {
         """
         val foo: Int = 1
         """
     }
 
+    @Test
     fun testValueParameter() = assertKeys(
         "Int%param-x%fun-Foo"
     ) {
@@ -78,6 +86,7 @@
         """
     }
 
+    @Test
     fun testAnnotation() = assertKeys {
         """
         annotation class Foo(val value: Int = 1)
@@ -87,6 +96,7 @@
     }
 
     // NOTE(lmr): In the future we should try and get this to work
+    @Test
     fun testForLoop() = assertKeys {
         """
         fun Foo() {
@@ -97,6 +107,7 @@
         """
     }
 
+    @Test
     fun testWhileTrue() = assertKeys(
         "Double%arg-1%call-greater%cond%if%body%loop%fun-Foo",
         "Int%arg-0%call-print%body%loop%fun-Foo"
@@ -111,6 +122,7 @@
         """
     }
 
+    @Test
     fun testWhileCondition() = assertKeys(
         "Int%arg-0%call-print%body%loop%fun-Foo"
     ) {
@@ -123,6 +135,7 @@
         """
     }
 
+    @Test
     fun testForInCollection() = assertKeys(
         "Int%arg-0%call-print-1%body%loop%fun-Foo"
     ) {
@@ -137,12 +150,14 @@
     }
 
     // NOTE(lmr): we should deal with this in some cases, but leaving untouched for now
+    @Test
     fun testConstantProperty() = assertKeys {
         """
         const val foo = 1
         """
     }
 
+    @Test
     fun testSafeCall() = assertKeys(
         "Boolean%arg-1%call-EQEQ%fun-Foo",
         "String%arg-0%call-contains%else%when%arg-0%call-EQEQ%fun-Foo"
@@ -154,6 +169,7 @@
         """
     }
 
+    @Test
     fun testElvis() = assertKeys(
         "String%branch%when%fun-Foo"
     ) {
@@ -164,6 +180,7 @@
         """
     }
 
+    @Test
     fun testTryCatch() = assertKeys(
         "Int%arg-0%call-invoke%catch%fun-Foo",
         "Int%arg-0%call-invoke%finally%fun-Foo",
@@ -182,6 +199,7 @@
         """
     }
 
+    @Test
     fun testWhen() = assertKeys(
         "Double%arg-1%call-greater%cond%when%fun-Foo",
         "Double%arg-1%call-greater%cond-1%when%fun-Foo",
@@ -200,6 +218,7 @@
         """
     }
 
+    @Test
     fun testWhenWithSubject() = assertKeys(
         "Double%%%this%call-rangeTo%%this%call-contains%cond%when%fun-Foo",
         "Double%%%this%call-rangeTo%%this%call-contains%cond-1%when%fun-Foo",
@@ -220,6 +239,7 @@
         """
     }
 
+    @Test
     fun testWhenWithSubject2() = assertKeys(
         "Int%arg-0%call-print%branch-1%when%fun-Foo",
         "Int%arg-0%call-print%else%when%fun-Foo",
@@ -236,6 +256,7 @@
         """
     }
 
+    @Test
     fun testDelegatingCtor() = assertKeys(
         "Int%arg-0%call-%init%%class-Bar"
     ) {
@@ -245,6 +266,7 @@
         """
     }
 
+    @Test
     fun testLocalVal() = assertKeys(
         "Int%arg-0%call-plus%set-y%fun-Foo",
         "Int%val-x%fun-Foo",
@@ -259,6 +281,7 @@
         """
     }
 
+    @Test
     fun testCapturedVar() = assertKeys(
         "Int%val-a%fun-Example",
         "String%0%str%fun-Example",
@@ -304,6 +327,7 @@
         """
     }
 
+    @Test
     fun testCommentsAbove() = assertDurableChange(
         """
             fun Test() {
@@ -318,6 +342,7 @@
         """.trimIndent()
     )
 
+    @Test
     fun testValsAndStructureAbove() = assertDurableChange(
         """
             fun Test() {
diff --git a/compose/compiler/compiler-hosted/integration-tests/src/test/java/androidx/compose/compiler/plugins/kotlin/LiveLiteralV2TransformTests.kt b/compose/compiler/compiler-hosted/integration-tests/src/test/java/androidx/compose/compiler/plugins/kotlin/LiveLiteralV2TransformTests.kt
index 58c6696..c4856fb 100644
--- a/compose/compiler/compiler-hosted/integration-tests/src/test/java/androidx/compose/compiler/plugins/kotlin/LiveLiteralV2TransformTests.kt
+++ b/compose/compiler/compiler-hosted/integration-tests/src/test/java/androidx/compose/compiler/plugins/kotlin/LiveLiteralV2TransformTests.kt
@@ -16,12 +16,15 @@
 
 package androidx.compose.compiler.plugins.kotlin
 
+import org.jetbrains.kotlin.config.CompilerConfiguration
 import org.junit.Test
 
 class LiveLiteralV2TransformTests : AbstractLiveLiteralTransformTests() {
-    override val liveLiteralsV2Enabled: Boolean
-        get() = true
+    override fun CompilerConfiguration.updateConfiguration() {
+        put(ComposeConfiguration.LIVE_LITERALS_V2_ENABLED_KEY, true)
+    }
 
+    @Test
     fun testSiblingCallArgs() = assertNoDuplicateKeys(
         """
         fun Test() {
@@ -31,6 +34,7 @@
         """
     )
 
+    @Test
     fun testFunctionCallWithConstArg() = assertKeys(
         "Int%arg-0%call-print%fun-Test",
         "Int%arg-0%call-print-1%fun-Test"
@@ -43,6 +47,7 @@
         """
     }
 
+    @Test
     fun testDispatchReceiver() = assertKeys(
         "Int%%this%call-toString%arg-0%call-print%fun-Test",
         "Int%arg-0%call-print-1%fun-Test"
@@ -55,6 +60,7 @@
         """
     }
 
+    @Test
     fun testInsidePropertyGetter() = assertKeys(
         "Int%fun-%get-foo%%get%val-foo"
     ) {
@@ -64,12 +70,14 @@
     }
 
     // NOTE(lmr): For static initializer expressions we can/should do more.
+    @Test
     fun testInsidePropertyInitializer() = assertKeys {
         """
         val foo: Int = 1
         """
     }
 
+    @Test
     fun testValueParameter() = assertKeys(
         "Int%param-x%fun-Foo"
     ) {
@@ -78,6 +86,7 @@
         """
     }
 
+    @Test
     fun testAnnotation() = assertKeys {
         """
         annotation class Foo(val value: Int = 1)
@@ -87,6 +96,7 @@
     }
 
     // NOTE(lmr): In the future we should try and get this to work
+    @Test
     fun testForLoop() = assertKeys {
         """
         fun Foo() {
@@ -97,6 +107,7 @@
         """
     }
 
+    @Test
     fun testWhileTrue() = assertKeys(
         "Double%arg-1%call-greater%cond%if%body%loop%fun-Foo",
         "Int%arg-0%call-print%body%loop%fun-Foo"
@@ -111,6 +122,7 @@
         """
     }
 
+    @Test
     fun testWhileCondition() = assertKeys(
         "Int%arg-0%call-print%body%loop%fun-Foo"
     ) {
@@ -123,6 +135,7 @@
         """
     }
 
+    @Test
     fun testForInCollection() = assertKeys(
         "Int%arg-0%call-print-1%body%loop%fun-Foo"
     ) {
@@ -137,12 +150,14 @@
     }
 
     // NOTE(lmr): we should deal with this in some cases, but leaving untouched for now
+    @Test
     fun testConstantProperty() = assertKeys {
         """
         const val foo = 1
         """
     }
 
+    @Test
     fun testSafeCall() = assertKeys(
         "Boolean%arg-1%call-EQEQ%fun-Foo",
         "String%arg-0%call-contains%else%when%arg-0%call-EQEQ%fun-Foo"
@@ -154,6 +169,7 @@
         """
     }
 
+    @Test
     fun testElvis() = assertKeys(
         "String%branch%when%fun-Foo"
     ) {
@@ -164,6 +180,7 @@
         """
     }
 
+    @Test
     fun testTryCatch() = assertKeys(
         "Int%arg-0%call-invoke%catch%fun-Foo",
         "Int%arg-0%call-invoke%finally%fun-Foo",
@@ -182,6 +199,7 @@
         """
     }
 
+    @Test
     fun testWhen() = assertKeys(
         "Double%arg-1%call-greater%cond%when%fun-Foo",
         "Double%arg-1%call-greater%cond-1%when%fun-Foo",
@@ -200,6 +218,7 @@
         """
     }
 
+    @Test
     fun testWhenWithSubject() = assertKeys(
         "Double%%%this%call-rangeTo%%this%call-contains%cond%when%fun-Foo",
         "Double%%%this%call-rangeTo%%this%call-contains%cond-1%when%fun-Foo",
@@ -220,6 +239,7 @@
         """
     }
 
+    @Test
     fun testWhenWithSubject2() = assertKeys(
         "Int%arg-0%call-print%branch-1%when%fun-Foo",
         "Int%arg-0%call-print%else%when%fun-Foo",
@@ -236,6 +256,7 @@
         """
     }
 
+    @Test
     fun testDelegatingCtor() = assertKeys(
         "Int%arg-0%call-%init%%class-Bar"
     ) {
@@ -245,6 +266,7 @@
         """
     }
 
+    @Test
     fun testLocalVal() = assertKeys(
         "Int%arg-0%call-plus%set-y%fun-Foo",
         "Int%val-x%fun-Foo",
@@ -259,6 +281,7 @@
         """
     }
 
+    @Test
     fun testCapturedVar() = assertKeys(
         "Int%val-a%fun-Example",
         "String%0%str%fun-Example",
@@ -304,6 +327,7 @@
         """
     }
 
+    @Test
     fun testCommentsAbove() = assertDurableChange(
         """
             fun Test() {
@@ -318,6 +342,7 @@
         """.trimIndent()
     )
 
+    @Test
     fun testValsAndStructureAbove() = assertDurableChange(
         """
             fun Test() {
diff --git a/compose/compiler/compiler-hosted/integration-tests/src/test/java/androidx/compose/compiler/plugins/kotlin/RememberIntrinsicTransformTests.kt b/compose/compiler/compiler-hosted/integration-tests/src/test/java/androidx/compose/compiler/plugins/kotlin/RememberIntrinsicTransformTests.kt
index c421acc..8fb61ca 100644
--- a/compose/compiler/compiler-hosted/integration-tests/src/test/java/androidx/compose/compiler/plugins/kotlin/RememberIntrinsicTransformTests.kt
+++ b/compose/compiler/compiler-hosted/integration-tests/src/test/java/androidx/compose/compiler/plugins/kotlin/RememberIntrinsicTransformTests.kt
@@ -20,9 +20,6 @@
 import org.junit.Test
 
 class RememberIntrinsicTransformTests : AbstractIrTransformTest() {
-    override val intrinsicRememberEnabled: Boolean
-        get() = true
-
     private fun comparisonPropagation(
         @Language("kotlin")
         unchecked: String,
diff --git a/compose/compiler/compiler-hosted/integration-tests/src/test/java/androidx/compose/compiler/plugins/kotlin/RunComposableTests.kt b/compose/compiler/compiler-hosted/integration-tests/src/test/java/androidx/compose/compiler/plugins/kotlin/RunComposableTests.kt
index 5390e8c..3bfd3f1 100644
--- a/compose/compiler/compiler-hosted/integration-tests/src/test/java/androidx/compose/compiler/plugins/kotlin/RunComposableTests.kt
+++ b/compose/compiler/compiler-hosted/integration-tests/src/test/java/androidx/compose/compiler/plugins/kotlin/RunComposableTests.kt
@@ -33,19 +33,18 @@
 import org.jetbrains.kotlin.config.CompilerConfiguration
 import org.junit.Test
 import java.net.URLClassLoader
+import org.junit.Assert.assertEquals
 
 class RunComposableTests : AbstractCodegenTest() {
-
-    override fun updateConfiguration(configuration: CompilerConfiguration) {
-        super.updateConfiguration(configuration)
-        configuration.setupLanguageVersionSettings(K2JVMCompilerArguments().apply {
+    override fun CompilerConfiguration.updateConfiguration() {
+        setupLanguageVersionSettings(K2JVMCompilerArguments().apply {
             // enabling multiPlatform to use expect/actual declarations
             multiPlatform = true
         })
     }
 
     @Test // Bug report: https://github.com/JetBrains/compose-jb/issues/1407
-    fun testDefaultValuesFromExpectComposableFunctions() = ensureSetup {
+    fun testDefaultValuesFromExpectComposableFunctions() {
         runCompose(
             testFunBody = """
                 ExpectComposable { value ->
diff --git a/compose/compiler/compiler-hosted/integration-tests/src/test/java/androidx/compose/compiler/plugins/kotlin/SanityCheckCodegenTests.kt b/compose/compiler/compiler-hosted/integration-tests/src/test/java/androidx/compose/compiler/plugins/kotlin/SanityCheckCodegenTests.kt
index 45402ec..afce655 100644
--- a/compose/compiler/compiler-hosted/integration-tests/src/test/java/androidx/compose/compiler/plugins/kotlin/SanityCheckCodegenTests.kt
+++ b/compose/compiler/compiler-hosted/integration-tests/src/test/java/androidx/compose/compiler/plugins/kotlin/SanityCheckCodegenTests.kt
@@ -16,9 +16,11 @@
 
 package androidx.compose.compiler.plugins.kotlin
 
-class SanityCheckCodegenTests : AbstractCodegenTest() {
+import org.junit.Test
 
-    fun testCallAbstractSuperWithTypeParameters() = ensureSetup {
+class SanityCheckCodegenTests : AbstractCodegenTest() {
+    @Test
+    fun testCallAbstractSuperWithTypeParameters() {
         testCompile(
             """
                 abstract class AbstractB<Type>(d: Type) : AbstractA<Int, Type>(d) {
@@ -35,7 +37,8 @@
 
     // Regression test, because we didn't have a test to catch a breakage introduced by
     // https://github.com/JetBrains/kotlin/commit/ae608ea67fc589c4472657dc0317e97cb67dd158
-    fun testNothings() = ensureSetup {
+    @Test
+    fun testNothings() {
         testCompile(
             """
                 import androidx.compose.runtime.Composable
@@ -59,7 +62,8 @@
     }
 
     // Regression test for b/222979253
-    fun testLabeledLambda() = ensureSetup {
+    @Test
+    fun testLabeledLambda() {
         testCompile(
             """
                 import androidx.compose.runtime.Composable
@@ -76,7 +80,8 @@
     }
 
     // Regression test for b/180168881
-    fun testFunctionReferenceWithinInferredComposableLambda() = ensureSetup {
+    @Test
+    fun testFunctionReferenceWithinInferredComposableLambda() {
         testCompile(
             """
                 import androidx.compose.runtime.Composable
@@ -92,7 +97,8 @@
     }
 
     // Regression test for KT-52843
-    fun testParameterInlineCaptureLambda() = ensureSetup {
+    @Test
+    fun testParameterInlineCaptureLambda() {
         testCompile(
             """
             import androidx.compose.runtime.Composable
@@ -117,6 +123,7 @@
     }
 
     // Regression validating b/237863365
+    @Test
     fun testComposableAsLastStatementInUnitReturningLambda() {
         testCompile(
             """
diff --git a/compose/compiler/compiler-hosted/integration-tests/src/test/java/androidx/compose/compiler/plugins/kotlin/ScopeComposabilityTests.kt b/compose/compiler/compiler-hosted/integration-tests/src/test/java/androidx/compose/compiler/plugins/kotlin/ScopeComposabilityTests.kt
index 5f00b26..a21119e 100644
--- a/compose/compiler/compiler-hosted/integration-tests/src/test/java/androidx/compose/compiler/plugins/kotlin/ScopeComposabilityTests.kt
+++ b/compose/compiler/compiler-hosted/integration-tests/src/test/java/androidx/compose/compiler/plugins/kotlin/ScopeComposabilityTests.kt
@@ -16,19 +16,20 @@
 
 package androidx.compose.compiler.plugins.kotlin
 
+import androidx.compose.compiler.plugins.kotlin.facade.SourceFile
 import com.intellij.psi.PsiElement
 import org.jetbrains.kotlin.psi.KtElement
-import org.jetbrains.kotlin.psi.KtFile
 import org.jetbrains.kotlin.psi.KtFunction
 import org.jetbrains.kotlin.psi.KtFunctionLiteral
 import org.jetbrains.kotlin.psi.KtLambdaExpression
 import org.jetbrains.kotlin.psi.KtProperty
 import org.jetbrains.kotlin.psi.KtPropertyAccessor
-import org.jetbrains.kotlin.psi.KtPsiFactory
 import org.jetbrains.kotlin.resolve.BindingContext
+import org.junit.Assert.assertTrue
+import org.junit.Test
 
 class ScopeComposabilityTests : AbstractCodegenTest() {
-
+    @Test
     fun testNormalFunctions() = assertComposability(
         """
             import androidx.compose.runtime.*
@@ -43,6 +44,7 @@
         """
     )
 
+    @Test
     fun testPropGetter() = assertComposability(
         """
             import androidx.compose.runtime.*
@@ -51,6 +53,7 @@
         """
     )
 
+    @Test
     fun testBasicComposable() = assertComposability(
         """
             import androidx.compose.runtime.*
@@ -62,6 +65,7 @@
         """
     )
 
+    @Test
     fun testBasicComposable2() = assertComposability(
         """
             import androidx.compose.runtime.*
@@ -81,6 +85,7 @@
 
     // We only analyze scopes that contain composable calls, so this test fails without the
     // nested call to `Bar`. This is why this test was originally muted (b/147250515).
+    @Test
     fun testBasicComposable3() = assertComposability(
         """
             import androidx.compose.runtime.*
@@ -95,6 +100,7 @@
         """
     )
 
+    @Test
     fun testBasicComposable4() = assertComposability(
         """
             import androidx.compose.runtime.*
@@ -114,6 +120,7 @@
         """
     )
 
+    @Test
     fun testBasicComposable5() = assertComposability(
         """
             import androidx.compose.runtime.*
@@ -130,6 +137,7 @@
         """
     )
 
+    @Test
     fun testBasicComposable6() = assertComposability(
         """
             import androidx.compose.runtime.*
@@ -148,16 +156,12 @@
     private fun assertComposability(srcText: String) {
         val (text, carets) = extractCarets(srcText)
 
-        val environment = myEnvironment ?: error("Environment not initialized")
-
-        val ktFile = KtPsiFactory(environment.project).createFile(text)
-        val bindingContext = JvmResolveUtil.analyzeAndCheckForErrors(
-            environment,
-            listOf(ktFile),
-        ).bindingContext
+        val analysisResult = analyze(listOf(SourceFile("test.kt", text)))
+        val bindingContext = analysisResult.bindingContext!!
+        val ktFile = analysisResult.files.single()
 
         carets.forEachIndexed { index, (offset, marking) ->
-            val composable = composabilityAtOffset(bindingContext, ktFile, offset)
+            val composable = ktFile.findElementAt(offset)!!.getNearestComposability(bindingContext)
 
             when (marking) {
                 "<composable>" -> assertTrue("index: $index", composable)
@@ -181,15 +185,6 @@
         }
         return src to indices
     }
-
-    private fun composabilityAtOffset(
-        bindingContext: BindingContext,
-        jetFile: KtFile,
-        index: Int
-    ): Boolean {
-        val element = jetFile.findElementAt(index)!!
-        return element.getNearestComposability(bindingContext)
-    }
 }
 
 fun PsiElement?.getNearestComposability(
diff --git a/compose/compiler/compiler-hosted/integration-tests/src/test/java/androidx/compose/compiler/plugins/kotlin/StaticExpressionDetectionTests.kt b/compose/compiler/compiler-hosted/integration-tests/src/test/java/androidx/compose/compiler/plugins/kotlin/StaticExpressionDetectionTests.kt
index 46267d4..65d89e7 100644
--- a/compose/compiler/compiler-hosted/integration-tests/src/test/java/androidx/compose/compiler/plugins/kotlin/StaticExpressionDetectionTests.kt
+++ b/compose/compiler/compiler-hosted/integration-tests/src/test/java/androidx/compose/compiler/plugins/kotlin/StaticExpressionDetectionTests.kt
@@ -16,10 +16,13 @@
 
 package androidx.compose.compiler.plugins.kotlin
 
+import androidx.compose.compiler.plugins.kotlin.facade.SourceFile
 import androidx.compose.compiler.plugins.kotlin.lower.dumpSrc
 import org.intellij.lang.annotations.Language
 import org.jetbrains.kotlin.ir.declarations.IrFunction
 import org.jetbrains.kotlin.ir.util.nameForIrSerialization
+import org.junit.Assert.assertEquals
+import org.junit.Assert.fail
 import org.junit.Test
 
 class StaticExpressionDetectionTests : AbstractIrTransformTest() {
@@ -273,8 +276,8 @@
         """.trimIndent()
 
         val files = listOf(
-            sourceFile("ExtraSrc.kt", extraSrc.replace('%', '$')),
-            sourceFile("Test.kt", source.replace('%', '$')),
+            SourceFile("ExtraSrc.kt", extraSrc),
+            SourceFile("Test.kt", source),
         )
         val irModule = compileToIr(files)
 
diff --git a/compose/compiler/compiler-hosted/integration-tests/src/test/java/androidx/compose/compiler/plugins/kotlin/TargetAnnotationsTransformTests.kt b/compose/compiler/compiler-hosted/integration-tests/src/test/java/androidx/compose/compiler/plugins/kotlin/TargetAnnotationsTransformTests.kt
index bfa67bd..90b67be 100644
--- a/compose/compiler/compiler-hosted/integration-tests/src/test/java/androidx/compose/compiler/plugins/kotlin/TargetAnnotationsTransformTests.kt
+++ b/compose/compiler/compiler-hosted/integration-tests/src/test/java/androidx/compose/compiler/plugins/kotlin/TargetAnnotationsTransformTests.kt
@@ -232,14 +232,9 @@
               traceEventStart(<>, %changed, -1, <>)
             }
             InlineRow({ %composer: Composer?, %changed: Int ->
-              %composer.startReplaceableGroup(<>)
-              sourceInformation(%composer, "C<Text("...>:Test.kt")
-              if (%changed and 0b1011 !== 0b0010 || !%composer.skipping) {
-                Text("test", %composer, 0b0110)
-              } else {
-                %composer.skipToGroupEnd()
-              }
-              %composer.endReplaceableGroup()
+              sourceInformationMarkerStart(%composer, <>, "C<Text("...>:Test.kt")
+              Text("test", %composer, 0b0110)
+              sourceInformationMarkerEnd(%composer)
             }, %composer, 0)
             if (isTraceInProgress()) {
               traceEventEnd()
@@ -518,6 +513,7 @@
         """
     )
 
+    @Test
     fun testLetIt() = verifyComposeIrTransform(
         """
         import androidx.compose.runtime.*
@@ -914,14 +910,9 @@
           }
           val tmp0_measurePolicy = localBoxMeasurePolicy
           Layout({ %composer: Composer?, %changed: Int ->
-            %composer.startReplaceableGroup(<>)
-            sourceInformation(%composer, "C<conten...>:Test.kt")
-            if (%changed and 0b1011 !== 0b0010 || !%composer.skipping) {
-              content(LocalBoxScopeInstance, %composer, 0b0110 or 0b01110000 and %changed)
-            } else {
-              %composer.skipToGroupEnd()
-            }
-            %composer.endReplaceableGroup()
+            sourceInformationMarkerStart(%composer, <>, "C<conten...>:Test.kt")
+            content(LocalBoxScopeInstance, %composer, 0b0110 or 0b01110000 and %changed)
+            sourceInformationMarkerEnd(%composer)
           }, modifier, tmp0_measurePolicy, %composer, 0b000110000000 or 0b01110000 and %changed shl 0b0011, 0)
           %composer.endReplaceableGroup()
         }
@@ -989,14 +980,9 @@
               traceEventStart(<>, %changed, -1, <>)
             }
             Layout({ %composer: Composer?, %changed: Int ->
-              %composer.startReplaceableGroup(<>)
-              sourceInformation(%composer, "C:Test.kt")
-              if (%changed and 0b1011 !== 0b0010 || !%composer.skipping) {
-                Unit
-              } else {
-                %composer.skipToGroupEnd()
-              }
-              %composer.endReplaceableGroup()
+              sourceInformationMarkerStart(%composer, <>, "C:Test.kt")
+              Unit
+              sourceInformationMarkerEnd(%composer)
             }, null, class <no name provided> : MeasurePolicy {
               override fun measure(%this%Layout: MeasureScope, <anonymous parameter 0>: List<Measurable>, <anonymous parameter 1>: Constraints): MeasureResult {
                 return error("")
diff --git a/compose/compiler/compiler-hosted/integration-tests/src/test/java/androidx/compose/compiler/plugins/kotlin/TraceInformationTest.kt b/compose/compiler/compiler-hosted/integration-tests/src/test/java/androidx/compose/compiler/plugins/kotlin/TraceInformationTest.kt
index 1bebc11..fe08eba 100644
--- a/compose/compiler/compiler-hosted/integration-tests/src/test/java/androidx/compose/compiler/plugins/kotlin/TraceInformationTest.kt
+++ b/compose/compiler/compiler-hosted/integration-tests/src/test/java/androidx/compose/compiler/plugins/kotlin/TraceInformationTest.kt
@@ -129,25 +129,20 @@
                 }
                 A(%composer, 0)
                 Wrapper({ %composer: Composer?, %changed: Int ->
-                  %composer.startReplaceableGroup(<>)
-                  sourceInformation(%composer, "C<A()>,<A()>:Test.kt")
-                  if (%changed and 0b1011 !== 0b0010 || !%composer.skipping) {
-                    A(%composer, 0)
-                    if (!condition) {
-                      %composer.endToMarker(tmp0_marker)
-                      if (isTraceInProgress()) {
-                        traceEventEnd()
-                      }
-                      %composer.endRestartGroup()?.updateScope { %composer: Composer?, %force: Int ->
-                        Test(condition, %composer, updateChangedFlags(%changed or 0b0001))
-                      }
-                      return
+                  sourceInformationMarkerStart(%composer, <>, "C<A()>,<A()>:Test.kt")
+                  A(%composer, 0)
+                  if (!condition) {
+                    %composer.endToMarker(tmp0_marker)
+                    if (isTraceInProgress()) {
+                      traceEventEnd()
                     }
-                    A(%composer, 0)
-                  } else {
-                    %composer.skipToGroupEnd()
+                    %composer.endRestartGroup()?.updateScope { %composer: Composer?, %force: Int ->
+                      Test(condition, %composer, updateChangedFlags(%changed or 0b0001))
+                    }
+                    return
                   }
-                  %composer.endReplaceableGroup()
+                  A(%composer, 0)
+                  sourceInformationMarkerEnd(%composer)
                 }, %composer, 0)
                 A(%composer, 0)
                 if (isTraceInProgress()) {
diff --git a/compose/compiler/compiler-hosted/integration-tests/src/test/java/androidx/compose/compiler/plugins/kotlin/analysis/ComposableCheckerTests.kt b/compose/compiler/compiler-hosted/integration-tests/src/test/java/androidx/compose/compiler/plugins/kotlin/analysis/ComposableCheckerTests.kt
index df5d016..1cdc4f6 100644
--- a/compose/compiler/compiler-hosted/integration-tests/src/test/java/androidx/compose/compiler/plugins/kotlin/analysis/ComposableCheckerTests.kt
+++ b/compose/compiler/compiler-hosted/integration-tests/src/test/java/androidx/compose/compiler/plugins/kotlin/analysis/ComposableCheckerTests.kt
@@ -1,68 +1,10 @@
 package androidx.compose.compiler.plugins.kotlin.analysis
 
 import androidx.compose.compiler.plugins.kotlin.AbstractComposeDiagnosticsTest
-import androidx.compose.compiler.plugins.kotlin.newConfiguration
-import com.intellij.openapi.util.Disposer
-import org.jetbrains.kotlin.cli.jvm.compiler.EnvironmentConfigFiles
-import org.jetbrains.kotlin.cli.jvm.compiler.KotlinCoreEnvironment
-import org.jetbrains.kotlin.cli.jvm.config.addJvmClasspathRoots
-import org.jetbrains.kotlin.cli.jvm.config.configureJdkClasspathRoots
+import org.junit.Test
 
 class ComposableCheckerTests : AbstractComposeDiagnosticsTest() {
-    override fun setUp() {
-        // intentionally don't call super.setUp() here since we are recreating an environment
-        // every test
-        System.setProperty(
-            "user.dir",
-            homeDir
-        )
-        System.setProperty(
-            "idea.ignore.disabled.plugins",
-            "true"
-        )
-    }
-
-    private fun doTest(text: String, expectPass: Boolean) {
-        val disposable = TestDisposable()
-        val classPath = createClasspath()
-        val configuration = newConfiguration()
-        configuration.addJvmClasspathRoots(classPath)
-        configuration.configureJdkClasspathRoots()
-
-        val environment =
-            KotlinCoreEnvironment.createForTests(
-                disposable,
-                configuration,
-                EnvironmentConfigFiles.JVM_CONFIG_FILES
-            )
-        setupEnvironment(environment)
-
-        try {
-            doTest(text, environment)
-            if (!expectPass) {
-                throw ExpectedFailureException(
-                    "Test unexpectedly passed, but SHOULD FAIL"
-                )
-            }
-        } catch (e: ExpectedFailureException) {
-            throw e
-        } catch (e: Exception) {
-            if (expectPass) throw Exception(e)
-        } finally {
-            Disposer.dispose(disposable)
-        }
-    }
-
-    class ExpectedFailureException(message: String) : Exception(message)
-
-    private fun check(expectedText: String) {
-        doTest(expectedText, true)
-    }
-
-    private fun checkFail(expectedText: String) {
-        doTest(expectedText, false)
-    }
-
+    @Test
     fun testCfromNC() = check(
         """
         import androidx.compose.runtime.*
@@ -72,6 +14,7 @@
     """
     )
 
+    @Test
     fun testNCfromC() = check(
         """
         import androidx.compose.runtime.*
@@ -81,6 +24,7 @@
     """
     )
 
+    @Test
     fun testCfromC() = check(
         """
         import androidx.compose.runtime.*
@@ -90,6 +34,7 @@
     """
     )
 
+    @Test
     fun testCinCLambdaArg() = check(
         """
         import androidx.compose.runtime.*
@@ -103,6 +48,7 @@
     """
     )
 
+    @Test
     fun testCinInlinedNCLambdaArg() = check(
         """
         import androidx.compose.runtime.*
@@ -116,6 +62,7 @@
     """
     )
 
+    @Test
     fun testCinNoinlineNCLambdaArg() = check(
         """
         import androidx.compose.runtime.*
@@ -129,6 +76,7 @@
     """
     )
 
+    @Test
     fun testCinCrossinlineNCLambdaArg() = check(
         """
         import androidx.compose.runtime.*
@@ -142,6 +90,7 @@
     """
     )
 
+    @Test
     fun testCinNestedInlinedNCLambdaArg() = check(
         """
         import androidx.compose.runtime.*
@@ -157,6 +106,7 @@
     """
     )
 
+    @Test
     fun testCinLambdaArgOfNC() = check(
         """
         import androidx.compose.runtime.*
@@ -170,6 +120,7 @@
     """
     )
 
+    @Test
     fun testCinLambdaArgOfC() = check(
         """
         import androidx.compose.runtime.*
@@ -183,6 +134,7 @@
     """
     )
 
+    @Test
     fun testCinCPropGetter() = check(
         """
         import androidx.compose.runtime.*
@@ -191,6 +143,7 @@
     """
     )
 
+    @Test
     fun testCinNCPropGetter() = check(
         """
         import androidx.compose.runtime.*
@@ -199,6 +152,7 @@
     """
     )
 
+    @Test
     fun testCinTopLevelInitializer() = check(
         """
         import androidx.compose.runtime.*
@@ -207,6 +161,7 @@
     """
     )
 
+    @Test
     fun testCTypeAlias() = check(
         """
         import androidx.compose.runtime.*
@@ -221,6 +176,7 @@
     """
     )
 
+    @Test
     fun testCfromComposableFunInterface() = check(
         """
         import androidx.compose.runtime.Composable
@@ -230,6 +186,7 @@
     """
     )
 
+    @Test
     fun testCfromAnnotatedComposableFunInterface() = check(
         """
         import androidx.compose.runtime.Composable
@@ -242,6 +199,7 @@
     """
     )
 
+    @Test
     fun testCfromComposableFunInterfaceArgument() = check(
         """
         import androidx.compose.runtime.Composable
@@ -252,6 +210,7 @@
     """
     )
 
+    @Test
     fun testCfromComposableTypeAliasFunInterface() = check(
         """
         import androidx.compose.runtime.Composable
@@ -263,6 +222,7 @@
     """
     )
 
+    @Test
     fun testCfromNonComposableFunInterface() = check(
         """
         import androidx.compose.runtime.Composable
@@ -276,6 +236,7 @@
     """
     )
 
+    @Test
     fun testCfromNonComposableFunInterfaceArgument() = check(
         """
         import androidx.compose.runtime.Composable
@@ -290,6 +251,7 @@
     """
     )
 
+    @Test
     fun testPreventedCaptureOnInlineLambda() = check(
         """
         import androidx.compose.runtime.*
@@ -305,6 +267,7 @@
     """
     )
 
+    @Test
     fun testComposableReporting001() {
         checkFail(
             """
@@ -338,6 +301,7 @@
         )
     }
 
+    @Test
     fun testComposableReporting002() {
         checkFail(
             """
@@ -363,6 +327,7 @@
         )
     }
 
+    @Test
     fun testComposableReporting006() {
         checkFail(
             """
@@ -399,6 +364,7 @@
         )
     }
 
+    @Test
     fun testComposableReporting007() {
         checkFail(
             """
@@ -411,6 +377,7 @@
         )
     }
 
+    @Test
     fun testComposableReporting008() {
         checkFail(
             """
@@ -429,6 +396,7 @@
         )
     }
 
+    @Test
     fun testComposableReporting009() {
         check(
             """
@@ -448,6 +416,7 @@
         )
     }
 
+    @Test
     fun testComposableReporting017() {
         checkFail(
             """
@@ -485,6 +454,7 @@
         )
     }
 
+    @Test
     fun testComposableReporting018() {
         checkFail(
             """
@@ -514,6 +484,7 @@
         )
     }
 
+    @Test
     fun testComposableReporting021() {
         check(
             """
@@ -534,6 +505,7 @@
         )
     }
 
+    @Test
     fun testComposableReporting022() {
         check(
             """
@@ -553,6 +525,7 @@
         )
     }
 
+    @Test
     fun testComposableReporting023() {
         check(
             """
@@ -573,6 +546,7 @@
         )
     }
 
+    @Test
     fun testComposableReporting024() {
         check(
             """
@@ -595,6 +569,7 @@
         )
     }
 
+    @Test
     fun testComposableReporting024x() {
         check(
             """
@@ -610,6 +585,7 @@
         )
     }
 
+    @Test
     fun testComposableReporting025() {
         check(
             """
@@ -626,6 +602,7 @@
         )
     }
 
+    @Test
     fun testComposableReporting026() {
         check(
             """
@@ -647,6 +624,7 @@
         )
     }
 
+    @Test
     fun testComposableReporting027() {
         check(
             """
@@ -670,6 +648,7 @@
         )
     }
 
+    @Test
     fun testComposableReporting028() {
         checkFail(
             """
@@ -693,6 +672,7 @@
         )
     }
 
+    @Test
     fun testComposableReporting030() {
         check(
             """
@@ -707,6 +687,7 @@
         )
     }
 
+    @Test
     fun testComposableReporting032() {
         check(
             """
@@ -726,6 +707,7 @@
         )
     }
 
+    @Test
     fun testComposableReporting033() {
         check(
             """
@@ -745,6 +727,7 @@
         )
     }
 
+    @Test
     fun testComposableReporting034() {
         checkFail(
             """
@@ -774,6 +757,7 @@
         )
     }
 
+    @Test
     fun testComposableReporting035() {
         check(
             """
@@ -788,6 +772,7 @@
         )
     }
 
+    @Test
     fun testComposableReporting039() {
         check(
             """
@@ -809,6 +794,7 @@
         )
     }
 
+    @Test
     fun testComposableReporting041() {
         check(
             """
@@ -830,6 +816,7 @@
         )
     }
 
+    @Test
     fun testComposableReporting043() {
         check(
             """
@@ -845,6 +832,7 @@
         )
     }
 
+    @Test
     fun testComposableReporting044() {
         check(
             """
@@ -863,6 +851,7 @@
         )
     }
 
+    @Test
     fun testComposableReporting045() {
         check(
             """
@@ -878,6 +867,7 @@
         )
     }
 
+    @Test
     fun testComposableReporting048() {
         // Type inference for non-null @Composable lambdas
         checkFail(
@@ -967,6 +957,7 @@
         )
     }
 
+    @Test
     fun testComposableReporting049() {
         check(
             """
@@ -978,6 +969,7 @@
         )
     }
 
+    @Test
     fun testComposableReporting050() {
         check(
             """
@@ -1004,6 +996,7 @@
         )
     }
 
+    @Test
     fun testComposableReporting051() {
         checkFail(
             """
@@ -1061,6 +1054,7 @@
         )
     }
 
+    @Test
     fun testComposableReporting052() {
         check(
             """
@@ -1089,6 +1083,7 @@
         )
     }
 
+    @Test
     fun testComposableReporting053() {
         check(
             """
@@ -1104,6 +1099,7 @@
         )
     }
 
+    @Test
     fun testComposableReporting054() {
         check(
             """
@@ -1141,6 +1137,7 @@
         )
     }
 
+    @Test
     fun testComposableReporting055() {
         check(
             """
@@ -1173,6 +1170,7 @@
         )
     }
 
+    @Test
     fun testComposableReporting057() {
         check(
             """
@@ -1191,6 +1189,7 @@
         )
     }
 
+    @Test
     fun testDisallowComposableCallPropagation() = check(
         """
         import androidx.compose.runtime.*
@@ -1207,6 +1206,7 @@
     """
     )
 
+    @Test
     fun testComposableLambdaToAll() = check(
         """
         import androidx.compose.runtime.*
@@ -1218,6 +1218,7 @@
     """
     )
 
+    @Test
     fun testReadOnlyComposablePropagation() = check(
         """
         import androidx.compose.runtime.*
diff --git a/compose/compiler/compiler-hosted/integration-tests/src/test/java/androidx/compose/compiler/plugins/kotlin/analysis/ComposableDeclarationCheckerTests.kt b/compose/compiler/compiler-hosted/integration-tests/src/test/java/androidx/compose/compiler/plugins/kotlin/analysis/ComposableDeclarationCheckerTests.kt
index 2f620f9..3239060 100644
--- a/compose/compiler/compiler-hosted/integration-tests/src/test/java/androidx/compose/compiler/plugins/kotlin/analysis/ComposableDeclarationCheckerTests.kt
+++ b/compose/compiler/compiler-hosted/integration-tests/src/test/java/androidx/compose/compiler/plugins/kotlin/analysis/ComposableDeclarationCheckerTests.kt
@@ -17,10 +17,12 @@
 package androidx.compose.compiler.plugins.kotlin.analysis
 
 import androidx.compose.compiler.plugins.kotlin.AbstractComposeDiagnosticsTest
+import org.junit.Test
 
 class ComposableDeclarationCheckerTests : AbstractComposeDiagnosticsTest() {
+    @Test
     fun testPropertyWithInitializer() {
-        doTest(
+        check(
             """
             import androidx.compose.runtime.Composable
 
@@ -30,8 +32,9 @@
         )
     }
 
+    @Test
     fun testComposableFunctionReferences() {
-        doTest(
+        check(
             """
             import androidx.compose.runtime.Composable
 
@@ -49,8 +52,9 @@
         )
     }
 
+    @Test
     fun testNonComposableFunctionReferences() {
-        doTest(
+        check(
             """
             import androidx.compose.runtime.Composable
 
@@ -68,8 +72,9 @@
         )
     }
 
+    @Test
     fun testPropertyWithGetterAndSetter() {
-        doTest(
+        check(
             """
             import androidx.compose.runtime.Composable
 
@@ -88,8 +93,9 @@
         )
     }
 
+    @Test
     fun testPropertyGetterAllForms() {
-        doTest(
+        check(
             """
             import androidx.compose.runtime.Composable
 
@@ -104,8 +110,9 @@
         )
     }
 
+    @Test
     fun testSuspendComposable() {
-        doTest(
+        check(
             """
             import androidx.compose.runtime.Composable
 
@@ -127,15 +134,16 @@
         )
     }
 
+    @Test
     fun testComposableMainFun() {
-        doTest(
+        check(
             """
             import androidx.compose.runtime.Composable
 
             @Composable fun <!COMPOSABLE_FUN_MAIN!>main<!>() {}
         """
         )
-        doTest(
+        check(
             """
             import androidx.compose.runtime.Composable
 
@@ -144,7 +152,7 @@
             }
         """
         )
-        doTest(
+        check(
             """
             import androidx.compose.runtime.Composable
 
@@ -157,8 +165,9 @@
         )
     }
 
+    @Test
     fun testMissingComposableOnOverride() {
-        doTest(
+        check(
             """
             import androidx.compose.runtime.Composable
 
@@ -193,8 +202,9 @@
         )
     }
 
+    @Test
     fun testInferenceOverComplexConstruct1() {
-        doTest(
+        check(
             """
             import androidx.compose.runtime.Composable
             val composable: @Composable ()->Unit = if(true) { { } } else { { } }
@@ -202,8 +212,9 @@
         )
     }
 
+    @Test
     fun testInferenceOverComplexConstruct2() {
-        doTest(
+        check(
             """
             import androidx.compose.runtime.Composable
             @Composable fun foo() { }
@@ -212,8 +223,9 @@
         )
     }
 
+    @Test
     fun testInterfaceComposablesWithDefaultParameters() {
-        doTest(
+        check(
             """
             import androidx.compose.runtime.Composable
             interface A {
@@ -223,8 +235,9 @@
         )
     }
 
+    @Test
     fun testAbstractComposablesWithDefaultParameters() {
-        doTest(
+        check(
             """
             import androidx.compose.runtime.Composable
             abstract class A {
@@ -234,8 +247,9 @@
         )
     }
 
+    @Test
     fun testInterfaceComposablesWithoutDefaultParameters() {
-        doTest(
+        check(
             """
             import androidx.compose.runtime.Composable
             interface A {
@@ -245,8 +259,9 @@
         )
     }
 
+    @Test
     fun testAbstractComposablesWithoutDefaultParameters() {
-        doTest(
+        check(
             """
             import androidx.compose.runtime.Composable
             abstract class A {
@@ -256,8 +271,9 @@
         )
     }
 
+    @Test
     fun testOverrideWithoutComposeAnnotation() {
-        doTest(
+        check(
             """
                 import androidx.compose.runtime.Composable
                 interface Base {
diff --git a/compose/compiler/compiler-hosted/integration-tests/src/test/java/androidx/compose/compiler/plugins/kotlin/analysis/ComposableTargetCheckerTests.kt b/compose/compiler/compiler-hosted/integration-tests/src/test/java/androidx/compose/compiler/plugins/kotlin/analysis/ComposableTargetCheckerTests.kt
index d9f2f9e..8102666 100644
--- a/compose/compiler/compiler-hosted/integration-tests/src/test/java/androidx/compose/compiler/plugins/kotlin/analysis/ComposableTargetCheckerTests.kt
+++ b/compose/compiler/compiler-hosted/integration-tests/src/test/java/androidx/compose/compiler/plugins/kotlin/analysis/ComposableTargetCheckerTests.kt
@@ -17,51 +17,10 @@
 package androidx.compose.compiler.plugins.kotlin.analysis
 
 import androidx.compose.compiler.plugins.kotlin.AbstractComposeDiagnosticsTest
-import androidx.compose.compiler.plugins.kotlin.newConfiguration
-import com.intellij.openapi.util.Disposer
-import org.jetbrains.kotlin.cli.jvm.compiler.EnvironmentConfigFiles
-import org.jetbrains.kotlin.cli.jvm.compiler.KotlinCoreEnvironment
-import org.jetbrains.kotlin.cli.jvm.config.addJvmClasspathRoots
-import org.jetbrains.kotlin.cli.jvm.config.configureJdkClasspathRoots
+import org.junit.Test
 
 class ComposableTargetCheckerTests : AbstractComposeDiagnosticsTest() {
-    override fun setUp() {
-        // intentionally don't call super.setUp() here since we are recreating an environment
-        // every test
-        System.setProperty(
-            "user.dir",
-            homeDir
-        )
-        System.setProperty(
-            "idea.ignore.disabled.plugins",
-            "true"
-        )
-    }
-
-    private fun check(text: String) {
-        val disposable = TestDisposable()
-        val classPath = createClasspath()
-        val configuration = newConfiguration()
-        configuration.addJvmClasspathRoots(classPath)
-        configuration.configureJdkClasspathRoots()
-
-        val environment =
-            KotlinCoreEnvironment.createForTests(
-                disposable,
-                configuration,
-                EnvironmentConfigFiles.JVM_CONFIG_FILES
-            )
-        setupEnvironment(environment)
-
-        try {
-            doTest(text, environment)
-        } catch (e: ComposableCheckerTests.ExpectedFailureException) {
-            throw e
-        } finally {
-            Disposer.dispose(disposable)
-        }
-    }
-
+    @Test
     fun testExplicitTargetAnnotations() = check(
         """
         import androidx.compose.runtime.*
@@ -84,6 +43,7 @@
         """
     )
 
+    @Test
     fun testInferredTargets() = check(
         """
         import androidx.compose.runtime.*
@@ -110,6 +70,7 @@
         """
     )
 
+    @Test
     fun testInferBoundContainer() = check(
         """
         import androidx.compose.runtime.*
@@ -138,6 +99,7 @@
         """
     )
 
+    @Test
     fun testInferGenericContainer() = check(
         """
         import androidx.compose.runtime.*
@@ -200,6 +162,7 @@
         """
     )
 
+    @Test
     fun testReportExplicitFailure() = check(
         """
         import androidx.compose.runtime.*
@@ -216,6 +179,7 @@
         """
     )
 
+    @Test
     fun testReportDisagreementFailure() = check(
         """
         import androidx.compose.runtime.*
@@ -236,6 +200,7 @@
         """
     )
 
+    @Test
     fun testGenericDisagreement() = check(
         """
         import androidx.compose.runtime.*
@@ -263,6 +228,7 @@
         """
     )
 
+    @Test
     fun testFunInterfaceInference() = check(
         """
         import androidx.compose.runtime.*
@@ -329,6 +295,7 @@
         """
     )
 
+    @Test
     fun testFileScopeTargetDeclaration() = check(
         """
         @file:ComposableTarget("N")
@@ -346,6 +313,7 @@
         """
     )
 
+    @Test
     fun testTargetMarker() = check(
         """
         import androidx.compose.runtime.Composable
@@ -373,6 +341,7 @@
         """
     )
 
+    @Test
     fun testFileScopeTargetMarker() = check(
         """
         @file: NComposable
@@ -401,6 +370,7 @@
         """
     )
 
+    @Test
     fun testUiTextAndInvalid() = check(
         """
         import androidx.compose.runtime.Composable
@@ -418,6 +388,7 @@
         """
     )
 
+    @Test
     fun testOpenOverrideAttributesInheritTarget() = check(
         """
         import androidx.compose.runtime.Composable
@@ -444,6 +415,7 @@
         """
     )
 
+    @Test
     fun testOpenOverrideTargetsMustAgree() = check(
         """
         import androidx.compose.runtime.Composable
@@ -468,6 +440,7 @@
         """
     )
 
+    @Test
     fun testOpenOverrideInferredToAgree() = check(
         """
         import androidx.compose.runtime.Composable
diff --git a/compose/compiler/compiler-hosted/integration-tests/src/test/java/androidx/compose/compiler/plugins/kotlin/analysis/TryCatchComposableCheckerTests.kt b/compose/compiler/compiler-hosted/integration-tests/src/test/java/androidx/compose/compiler/plugins/kotlin/analysis/TryCatchComposableCheckerTests.kt
index 93fbd5e..790f010 100644
--- a/compose/compiler/compiler-hosted/integration-tests/src/test/java/androidx/compose/compiler/plugins/kotlin/analysis/TryCatchComposableCheckerTests.kt
+++ b/compose/compiler/compiler-hosted/integration-tests/src/test/java/androidx/compose/compiler/plugins/kotlin/analysis/TryCatchComposableCheckerTests.kt
@@ -17,15 +17,16 @@
 package androidx.compose.compiler.plugins.kotlin.analysis
 
 import androidx.compose.compiler.plugins.kotlin.AbstractComposeDiagnosticsTest
+import org.junit.Test
 
 /**
  * We're strongly considering supporting try-catch-finally blocks in the future.
  * If/when we do support them, these tests should be deleted.
  */
 class TryCatchComposableCheckerTests : AbstractComposeDiagnosticsTest() {
-
+    @Test
     fun testTryCatchReporting001() {
-        doTest(
+        check(
             """
             import androidx.compose.runtime.*;
 
@@ -41,8 +42,9 @@
         )
     }
 
+    @Test
     fun testTryCatchReporting002() {
-        doTest(
+        check(
             """
             import androidx.compose.runtime.*;
 
@@ -58,8 +60,9 @@
         )
     }
 
+    @Test
     fun testTryCatchReporting003() {
-        doTest(
+        check(
             """
             import androidx.compose.runtime.*;
 
@@ -77,8 +80,9 @@
         )
     }
 
+    @Test
     fun testTryCatchReporting004() {
-        doTest(
+        check(
             """
             import androidx.compose.runtime.*;
 
@@ -94,8 +98,9 @@
         )
     }
 
+    @Test
     fun testTryCatchReporting005() {
-        doTest(
+        check(
             """
             import androidx.compose.runtime.*
             var globalContent = @Composable {}
diff --git a/compose/compiler/compiler-hosted/integration-tests/src/test/java/androidx/compose/compiler/plugins/kotlin/debug/AbstractDebuggerTest.kt b/compose/compiler/compiler-hosted/integration-tests/src/test/java/androidx/compose/compiler/plugins/kotlin/debug/AbstractDebuggerTest.kt
index c63ff7c..51ed468 100644
--- a/compose/compiler/compiler-hosted/integration-tests/src/test/java/androidx/compose/compiler/plugins/kotlin/debug/AbstractDebuggerTest.kt
+++ b/compose/compiler/compiler-hosted/integration-tests/src/test/java/androidx/compose/compiler/plugins/kotlin/debug/AbstractDebuggerTest.kt
@@ -17,9 +17,11 @@
 package androidx.compose.compiler.plugins.kotlin.debug
 
 import androidx.compose.compiler.plugins.kotlin.AbstractCodegenTest
-import androidx.compose.compiler.plugins.kotlin.CodegenTestFiles
+import androidx.compose.compiler.plugins.kotlin.debug.clientserver.TestProcessServer
 import androidx.compose.compiler.plugins.kotlin.debug.clientserver.TestProxy
-import androidx.compose.compiler.plugins.kotlin.tmpDir
+import androidx.compose.compiler.plugins.kotlin.facade.SourceFile
+import com.intellij.util.PathUtil
+import com.intellij.util.SystemProperties
 import com.sun.jdi.AbsentInformationException
 import com.sun.jdi.VirtualMachine
 import com.sun.jdi.event.BreakpointEvent
@@ -35,13 +37,21 @@
 import com.sun.jdi.request.MethodEntryRequest
 import com.sun.jdi.request.MethodExitRequest
 import com.sun.jdi.request.StepRequest
+import com.sun.tools.jdi.SocketAttachingConnector
+import java.io.File
 import org.intellij.lang.annotations.Language
 import org.jetbrains.kotlin.backend.common.output.SimpleOutputFileCollection
 import org.jetbrains.kotlin.cli.common.output.writeAllTo
 import org.jetbrains.kotlin.codegen.GeneratedClassLoader
-import org.jetbrains.kotlin.psi.KtFile
 import java.net.URL
 import java.net.URLClassLoader
+import kotlin.properties.Delegates
+import org.junit.After
+import org.junit.AfterClass
+import org.junit.Before
+import org.junit.BeforeClass
+import org.junit.Rule
+import org.junit.rules.TemporaryFolder
 
 private const val RUNNER_CLASS = "RunnerKt"
 private const val MAIN_METHOD = "main"
@@ -49,29 +59,70 @@
 private const val TEST_CLASS = "TestKt"
 
 abstract class AbstractDebuggerTest : AbstractCodegenTest() {
-    private lateinit var virtualMachine: VirtualMachine
-    private var proxyPort: Int = -1
+    companion object {
+        private lateinit var testServerProcess: Process
+        lateinit var virtualMachine: VirtualMachine
+        var proxyPort: Int = -1
+
+        @JvmStatic
+        @BeforeClass
+        fun startDebugProcess() {
+            testServerProcess = startTestProcessServer()
+            val (debuggerPort, _proxyPort) = testServerProcess.inputStream.bufferedReader().use {
+                val debuggerPort = it.readLine().split("address:").last().trim().toInt()
+                it.readLine()
+                val proxyPort = it.readLine().split("port ").last().trim().toInt()
+                (debuggerPort to proxyPort)
+            }
+            virtualMachine = attachDebugger(debuggerPort)
+            proxyPort = _proxyPort
+        }
+
+        @JvmStatic
+        @AfterClass
+        fun stopDebugProcess() {
+            testServerProcess.destroy()
+        }
+
+        private fun startTestProcessServer(): Process {
+            val classpath = listOf(
+                PathUtil.getJarPathForClass(TestProcessServer::class.java),
+                PathUtil.getJarPathForClass(Delegates::class.java) // Add Kotlin runtime JAR
+            )
+
+            val javaExec = File(File(SystemProperties.getJavaHome(), "bin"), "java")
+            assert(javaExec.exists())
+
+            val command = listOf(
+                javaExec.absolutePath,
+                "-agentlib:jdwp=transport=dt_socket,server=y,suspend=n,address=127.0.0.1:0",
+                "-ea",
+                "-classpath", classpath.joinToString(File.pathSeparator),
+                TestProcessServer::class.qualifiedName,
+                TestProcessServer.DEBUG_TEST
+            )
+
+            return ProcessBuilder(command).start()
+        }
+
+        private const val DEBUG_ADDRESS = "127.0.0.1"
+
+        private fun attachDebugger(port: Int): VirtualMachine {
+            val connector = SocketAttachingConnector()
+            return connector.attach(
+                connector.defaultArguments().apply {
+                    getValue("port").setValue("$port")
+                    getValue("hostname").setValue(DEBUG_ADDRESS)
+                }
+            )
+        }
+    }
+
     private lateinit var methodEntryRequest: MethodEntryRequest
     private lateinit var methodExitRequest: MethodExitRequest
 
-    fun initialize(vm: VirtualMachine, port: Int) {
-        virtualMachine = vm
-        proxyPort = port
-    }
-
-    override fun setUp() {
-        super.setUp()
-        if (proxyPort == -1) error("initialize method must be called on AbstractDebuggerTest")
-        createMethodEventsForTestClass()
-    }
-
-    override fun tearDown() {
-        super.tearDown()
-        virtualMachine.eventRequestManager()
-            .deleteEventRequests(listOf(methodEntryRequest, methodExitRequest))
-    }
-
-    private fun createMethodEventsForTestClass() {
+    @Before
+    fun createMethodEventsForTestClass() {
         val manager = virtualMachine.eventRequestManager()
         methodEntryRequest = manager.createMethodEntryRequest()
         methodEntryRequest.addClassFilter(TEST_CLASS)
@@ -84,13 +135,23 @@
         methodExitRequest.enable()
     }
 
+    @After
+    fun deleteEventRequests() {
+        virtualMachine.eventRequestManager()
+            .deleteEventRequests(listOf(methodEntryRequest, methodExitRequest))
+    }
+
+    @JvmField
+    @Rule
+    val outDirectory = TemporaryFolder()
+
     private fun invokeRunnerMainInSeparateProcess(
         classLoader: URLClassLoader,
         port: Int
     ) {
         val classPath = classLoader.extractUrls().toMutableList()
         if (classLoader is GeneratedClassLoader) {
-            val outDir = tmpDir("${this::class.simpleName}_${this.name}")
+            val outDir = outDirectory.root
             val currentOutput = SimpleOutputFileCollection(classLoader.allGeneratedFiles)
             currentOutput.writeAllTo(outDir)
             classPath.add(0, outDir.toURI().toURL())
@@ -99,16 +160,12 @@
     }
 
     fun collectDebugEvents(@Language("kotlin") source: String): List<LocatableEvent> {
-        val files = mutableListOf<KtFile>()
-        files.addAll(helperFiles())
-        files.add(sourceFile("Runner.kt", RUNNER_SOURCES))
-        files.add(sourceFile("Test.kt", source))
-        myFiles = CodegenTestFiles.create(files)
-        return doTest()
-    }
-
-    private fun doTest(): List<LocatableEvent> {
-        val classLoader = createClassLoader()
+        val classLoader = createClassLoader(
+            listOf(
+                SourceFile("Runner.kt", RUNNER_SOURCES),
+                SourceFile("Test.kt", source)
+            )
+        )
         val testClass = classLoader.loadClass(TEST_CLASS)
         assert(testClass.declaredMethods.any { it.name == CONTENT_METHOD }) {
             "Test method $CONTENT_METHOD not present on test class $TEST_CLASS"
diff --git a/compose/compiler/compiler-hosted/integration-tests/src/test/java/androidx/compose/compiler/plugins/kotlin/debug/DebugTestSetup.kt b/compose/compiler/compiler-hosted/integration-tests/src/test/java/androidx/compose/compiler/plugins/kotlin/debug/DebugTestSetup.kt
deleted file mode 100644
index 96cc692..0000000
--- a/compose/compiler/compiler-hosted/integration-tests/src/test/java/androidx/compose/compiler/plugins/kotlin/debug/DebugTestSetup.kt
+++ /dev/null
@@ -1,91 +0,0 @@
-/*
- * Copyright 2021 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- *      http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-package androidx.compose.compiler.plugins.kotlin.debug
-
-import androidx.compose.compiler.plugins.kotlin.debug.clientserver.TestProcessServer
-import com.intellij.util.PathUtil
-import com.intellij.util.SystemProperties
-import com.sun.jdi.VirtualMachine
-import com.sun.tools.jdi.SocketAttachingConnector
-import junit.extensions.TestSetup
-import junit.framework.Test
-import java.io.File
-import kotlin.properties.Delegates
-
-/**
- * An utility that allows sharing of the [TestProcessServer] and debugger across multiple tests.
- * It startups [TestProcessServer] and attaches debugger to it.
- */
-class DebugTestSetup(
-    test: Test,
-    val onDebugEnvironmentAvailable: (DebugEnvironment) -> Unit
-) : TestSetup(test) {
-    private lateinit var testServerProcess: Process
-
-    override fun setUp() {
-        super.setUp()
-        testServerProcess = startTestProcessServer()
-        val (debuggerPort, proxyPort) = testServerProcess.inputStream.bufferedReader().use {
-            val debuggerPort = it.readLine().split("address:").last().trim().toInt()
-            it.readLine()
-            val proxyPort = it.readLine().split("port ").last().trim().toInt()
-            (debuggerPort to proxyPort)
-        }
-        val virtualMachine = attachDebugger(debuggerPort)
-        onDebugEnvironmentAvailable(DebugEnvironment(virtualMachine, proxyPort))
-    }
-
-    override fun tearDown() {
-        super.tearDown()
-        testServerProcess.destroy()
-    }
-}
-
-class DebugEnvironment(val virtualMachine: VirtualMachine, val proxyPort: Int)
-
-private fun startTestProcessServer(): Process {
-    val classpath = listOf(
-        PathUtil.getJarPathForClass(TestProcessServer::class.java),
-        PathUtil.getJarPathForClass(Delegates::class.java) // Add Kotlin runtime JAR
-    )
-
-    val javaExec = File(File(SystemProperties.getJavaHome(), "bin"), "java")
-    assert(javaExec.exists())
-
-    val command = listOf(
-        javaExec.absolutePath,
-        "-agentlib:jdwp=transport=dt_socket,server=y,suspend=n,address=127.0.0.1:0",
-        "-ea",
-        "-classpath", classpath.joinToString(File.pathSeparator),
-        TestProcessServer::class.qualifiedName,
-        TestProcessServer.DEBUG_TEST
-    )
-
-    return ProcessBuilder(command).start()
-}
-
-private const val DEBUG_ADDRESS = "127.0.0.1"
-
-private fun attachDebugger(port: Int): VirtualMachine {
-    val connector = SocketAttachingConnector()
-    return connector.attach(
-        connector.defaultArguments().apply {
-            getValue("port").setValue("$port")
-            getValue("hostname").setValue(DEBUG_ADDRESS)
-        }
-    )
-}
\ No newline at end of file
diff --git a/compose/compiler/compiler-hosted/integration-tests/src/test/java/androidx/compose/compiler/plugins/kotlin/debug/StepTest.kt b/compose/compiler/compiler-hosted/integration-tests/src/test/java/androidx/compose/compiler/plugins/kotlin/debug/StepTest.kt
index b64cd0f..23bb002 100644
--- a/compose/compiler/compiler-hosted/integration-tests/src/test/java/androidx/compose/compiler/plugins/kotlin/debug/StepTest.kt
+++ b/compose/compiler/compiler-hosted/integration-tests/src/test/java/androidx/compose/compiler/plugins/kotlin/debug/StepTest.kt
@@ -16,11 +16,10 @@
 
 package androidx.compose.compiler.plugins.kotlin.debug
 
-import junit.framework.Test
-import junit.framework.TestSuite
+import org.junit.Test
 
 class StepTest : AbstractDebuggerTest() {
-
+    @Test
     fun testSteppingIntoIf() {
         collectDebugEvents(
             """
@@ -49,17 +48,4 @@
             """.trimIndent()
         )
     }
-
-    companion object {
-        @JvmStatic
-        fun suite(): Test {
-            val testSuite = TestSuite(StepTest::class.java)
-            return DebugTestSetup(testSuite) { debugEnv ->
-                testSuite.tests().toList().filterIsInstance(AbstractDebuggerTest::class.java)
-                    .forEach {
-                        it.initialize(debugEnv.virtualMachine, debugEnv.proxyPort)
-                    }
-            }
-        }
-    }
 }
diff --git a/compose/compiler/compiler-hosted/integration-tests/src/test/java/androidx/compose/compiler/plugins/kotlin/facade/K1CompilerFacade.kt b/compose/compiler/compiler-hosted/integration-tests/src/test/java/androidx/compose/compiler/plugins/kotlin/facade/K1CompilerFacade.kt
new file mode 100644
index 0000000..de9ff6a
--- /dev/null
+++ b/compose/compiler/compiler-hosted/integration-tests/src/test/java/androidx/compose/compiler/plugins/kotlin/facade/K1CompilerFacade.kt
@@ -0,0 +1,133 @@
+/*
+ * Copyright 2022 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.compose.compiler.plugins.kotlin.facade
+
+import androidx.compose.compiler.plugins.kotlin.TestsCompilerError
+import org.jetbrains.kotlin.backend.common.phaser.PhaseConfig
+import org.jetbrains.kotlin.backend.jvm.JvmIrCodegenFactory
+import org.jetbrains.kotlin.backend.jvm.jvmPhases
+import org.jetbrains.kotlin.cli.common.CLIConfigurationKeys
+import org.jetbrains.kotlin.cli.jvm.compiler.CliBindingTrace
+import org.jetbrains.kotlin.cli.jvm.compiler.KotlinCoreEnvironment
+import org.jetbrains.kotlin.cli.jvm.compiler.TopDownAnalyzerFacadeForJVM
+import org.jetbrains.kotlin.codegen.ClassBuilderFactories
+import org.jetbrains.kotlin.codegen.CodegenFactory
+import org.jetbrains.kotlin.codegen.state.GenerationState
+import org.jetbrains.kotlin.descriptors.ModuleDescriptor
+import org.jetbrains.kotlin.ir.declarations.IrModuleFragment
+import org.jetbrains.kotlin.psi.KtFile
+import org.jetbrains.kotlin.resolve.AnalyzingUtils
+import org.jetbrains.kotlin.resolve.BindingContext
+
+class K1AnalysisResult(
+    override val files: List<KtFile>,
+    val moduleDescriptor: ModuleDescriptor,
+    override val bindingContext: BindingContext
+) : AnalysisResult {
+    override val diagnostics: List<AnalysisResult.Diagnostic>
+        get() = bindingContext.diagnostics.all().map {
+            AnalysisResult.Diagnostic(it.factoryName, it.textRanges)
+        }
+}
+
+class K1FrontendResult(
+    val state: GenerationState,
+    val backendInput: JvmIrCodegenFactory.JvmIrBackendInput,
+    val codegenFactory: JvmIrCodegenFactory
+)
+
+class K1CompilerFacade(environment: KotlinCoreEnvironment) : KotlinCompilerFacade(environment) {
+    override fun analyze(files: List<SourceFile>): K1AnalysisResult {
+        val ktFiles = files.map { it.toKtFile(environment.project) }
+        val result = TopDownAnalyzerFacadeForJVM.analyzeFilesWithJavaIntegration(
+            environment.project,
+            ktFiles,
+            CliBindingTrace(),
+            environment.configuration,
+            environment::createPackagePartProvider
+        )
+
+        try {
+            result.throwIfError()
+        } catch (e: Exception) {
+            throw TestsCompilerError(e)
+        }
+
+        return K1AnalysisResult(ktFiles, result.moduleDescriptor, result.bindingContext)
+    }
+
+    private fun frontend(files: List<SourceFile>): K1FrontendResult {
+        val analysisResult = analyze(files)
+
+        // `analyze` only throws if the analysis itself failed, since we use it to test code
+        // with errors. That's why we have to check for errors before we run psi2ir.
+        try {
+            AnalyzingUtils.throwExceptionOnErrors(analysisResult.bindingContext)
+        } catch (e: Exception) {
+            throw TestsCompilerError(e)
+        }
+
+        val codegenFactory = JvmIrCodegenFactory(
+            environment.configuration,
+            environment.configuration.get(CLIConfigurationKeys.PHASE_CONFIG)
+                ?: PhaseConfig(jvmPhases)
+        )
+
+        val state = GenerationState.Builder(
+            environment.project,
+            ClassBuilderFactories.TEST,
+            analysisResult.moduleDescriptor,
+            analysisResult.bindingContext,
+            analysisResult.files,
+            environment.configuration
+        ).isIrBackend(true).codegenFactory(codegenFactory).build()
+
+        state.beforeCompile()
+
+        val psi2irInput = CodegenFactory.IrConversionInput.fromGenerationStateAndFiles(
+            state,
+            analysisResult.files
+        )
+        val backendInput = codegenFactory.convertToIr(psi2irInput)
+
+        // For JVM-specific errors
+        try {
+            AnalyzingUtils.throwExceptionOnErrors(state.collectedExtraJvmDiagnostics)
+        } catch (e: Throwable) {
+            throw TestsCompilerError(e)
+        }
+
+        return K1FrontendResult(
+            state,
+            backendInput,
+            codegenFactory
+        )
+    }
+
+    override fun compileToIr(files: List<SourceFile>): IrModuleFragment =
+        frontend(files).backendInput.irModuleFragment
+
+    override fun compile(files: List<SourceFile>): GenerationState =
+        try {
+            frontend(files).apply {
+                codegenFactory.generateModule(state, backendInput)
+                state.factory.done()
+            }.state
+        } catch (e: Exception) {
+            throw TestsCompilerError(e)
+        }
+}
diff --git a/compose/compiler/compiler-hosted/integration-tests/src/test/java/androidx/compose/compiler/plugins/kotlin/facade/KotlinCompilerFacade.kt b/compose/compiler/compiler-hosted/integration-tests/src/test/java/androidx/compose/compiler/plugins/kotlin/facade/KotlinCompilerFacade.kt
new file mode 100644
index 0000000..6e77efc
--- /dev/null
+++ b/compose/compiler/compiler-hosted/integration-tests/src/test/java/androidx/compose/compiler/plugins/kotlin/facade/KotlinCompilerFacade.kt
@@ -0,0 +1,155 @@
+/*
+ * Copyright 2022 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.compose.compiler.plugins.kotlin.facade
+
+import androidx.compose.compiler.plugins.kotlin.ComposeComponentRegistrar
+import androidx.compose.compiler.plugins.kotlin.TestsCompilerError
+import com.intellij.openapi.Disposable
+import com.intellij.openapi.project.Project
+import com.intellij.openapi.util.TextRange
+import com.intellij.openapi.util.text.StringUtilRt
+import com.intellij.openapi.vfs.CharsetToolkit
+import com.intellij.psi.PsiFileFactory
+import com.intellij.psi.impl.PsiFileFactoryImpl
+import com.intellij.testFramework.LightVirtualFile
+import org.jetbrains.kotlin.cli.common.CLIConfigurationKeys
+import org.jetbrains.kotlin.cli.common.messages.CompilerMessageSeverity
+import org.jetbrains.kotlin.cli.common.messages.CompilerMessageSourceLocation
+import org.jetbrains.kotlin.cli.common.messages.IrMessageCollector
+import org.jetbrains.kotlin.cli.common.messages.MessageCollector
+import org.jetbrains.kotlin.cli.jvm.compiler.EnvironmentConfigFiles
+import org.jetbrains.kotlin.cli.jvm.compiler.KotlinCoreEnvironment
+import org.jetbrains.kotlin.codegen.state.GenerationState
+import org.jetbrains.kotlin.config.CommonConfigurationKeys
+import org.jetbrains.kotlin.config.CompilerConfiguration
+import org.jetbrains.kotlin.config.JVMConfigurationKeys
+import org.jetbrains.kotlin.config.JvmTarget
+import org.jetbrains.kotlin.idea.KotlinLanguage
+import org.jetbrains.kotlin.ir.declarations.IrModuleFragment
+import org.jetbrains.kotlin.ir.util.IrMessageLogger
+import org.jetbrains.kotlin.psi.KtFile
+import org.jetbrains.kotlin.resolve.AnalyzingUtils
+import org.jetbrains.kotlin.resolve.BindingContext
+
+class SourceFile(
+    val name: String,
+    val source: String,
+    private val ignoreParseErrors: Boolean = false
+) {
+    fun toKtFile(project: Project): KtFile {
+        val shortName = name.substring(name.lastIndexOf('/') + 1).let {
+            it.substring(it.lastIndexOf('\\') + 1)
+        }
+
+        val virtualFile = object : LightVirtualFile(
+            shortName,
+            KotlinLanguage.INSTANCE,
+            StringUtilRt.convertLineSeparators(source)
+        ) {
+            override fun getPath(): String = "/$name"
+        }
+
+        virtualFile.charset = CharsetToolkit.UTF8_CHARSET
+        val factory = PsiFileFactory.getInstance(project) as PsiFileFactoryImpl
+        val ktFile = factory.trySetupPsiForFile(
+            virtualFile, KotlinLanguage.INSTANCE, true, false
+        ) as KtFile
+
+        if (!ignoreParseErrors) {
+            try {
+                AnalyzingUtils.checkForSyntacticErrors(ktFile)
+            } catch (e: Exception) {
+                throw TestsCompilerError(e)
+            }
+        }
+        return ktFile
+    }
+}
+
+interface AnalysisResult {
+    data class Diagnostic(
+        val factoryName: String,
+        val textRanges: List<TextRange>
+    )
+
+    val files: List<KtFile>
+    val diagnostics: List<Diagnostic>
+    val bindingContext: BindingContext?
+}
+
+abstract class KotlinCompilerFacade(val environment: KotlinCoreEnvironment) {
+    abstract fun analyze(files: List<SourceFile>): AnalysisResult
+    abstract fun compileToIr(files: List<SourceFile>): IrModuleFragment
+    abstract fun compile(files: List<SourceFile>): GenerationState
+
+    companion object {
+        const val TEST_MODULE_NAME = "test-module"
+
+        fun create(
+            disposable: Disposable,
+            updateConfiguration: CompilerConfiguration.() -> Unit,
+            registerExtensions: Project.(CompilerConfiguration) -> Unit,
+        ): KotlinCompilerFacade {
+            val configuration = CompilerConfiguration().apply {
+                put(CommonConfigurationKeys.MODULE_NAME, TEST_MODULE_NAME)
+                put(JVMConfigurationKeys.IR, true)
+                put(JVMConfigurationKeys.VALIDATE_IR, true)
+                put(JVMConfigurationKeys.JVM_TARGET, JvmTarget.JVM_1_8)
+                put(CLIConfigurationKeys.MESSAGE_COLLECTOR_KEY, TestMessageCollector)
+                put(IrMessageLogger.IR_MESSAGE_LOGGER, IrMessageCollector(TestMessageCollector))
+                updateConfiguration()
+            }
+
+            val environment = KotlinCoreEnvironment.createForTests(
+                disposable, configuration, EnvironmentConfigFiles.JVM_CONFIG_FILES
+            )
+
+            ComposeComponentRegistrar.checkCompilerVersion(configuration)
+
+            environment.project.registerExtensions(configuration)
+
+            return if (configuration.getBoolean(CommonConfigurationKeys.USE_FIR)) {
+                error("FIR unsupported")
+            } else {
+                K1CompilerFacade(environment)
+            }
+        }
+    }
+}
+
+private object TestMessageCollector : MessageCollector {
+    override fun clear() {}
+
+    override fun report(
+        severity: CompilerMessageSeverity,
+        message: String,
+        location: CompilerMessageSourceLocation?
+    ) {
+        if (severity === CompilerMessageSeverity.ERROR) {
+            throw AssertionError(
+                if (location == null)
+                    message
+                else
+                    "(${location.path}:${location.line}:${location.column}) $message"
+            )
+        }
+    }
+
+    override fun hasErrors(): Boolean {
+        return false
+    }
+}
diff --git a/compose/compiler/compiler-hosted/src/main/java/androidx/compose/compiler/plugins/kotlin/ComposePlugin.kt b/compose/compiler/compiler-hosted/src/main/java/androidx/compose/compiler/plugins/kotlin/ComposePlugin.kt
index bba70db..2eb933f 100644
--- a/compose/compiler/compiler-hosted/src/main/java/androidx/compose/compiler/plugins/kotlin/ComposePlugin.kt
+++ b/compose/compiler/compiler-hosted/src/main/java/androidx/compose/compiler/plugins/kotlin/ComposePlugin.kt
@@ -198,70 +198,89 @@
     ) {
         if (checkCompilerVersion(configuration)) {
             registerCommonExtensions(project)
-            registerIrExtension(project, configuration)
+            IrGenerationExtension.registerExtension(
+                project,
+                createComposeIrExtension(configuration)
+            )
         }
     }
 
     companion object {
         fun checkCompilerVersion(configuration: CompilerConfiguration): Boolean {
-            val KOTLIN_VERSION_EXPECTATION = "1.8.0"
-            KotlinCompilerVersion.getVersion()?.let { version ->
-                val msgCollector = configuration.get(CLIConfigurationKeys.MESSAGE_COLLECTOR_KEY)
-                val suppressKotlinVersionCheck = configuration.get(
-                    ComposeConfiguration.SUPPRESS_KOTLIN_VERSION_COMPATIBILITY_CHECK
-                )
-                if (suppressKotlinVersionCheck != null && suppressKotlinVersionCheck != version) {
-                    if (suppressKotlinVersionCheck == "true") {
+            try {
+                val KOTLIN_VERSION_EXPECTATION = "1.8.0"
+                KotlinCompilerVersion.getVersion()?.let { version ->
+                    val msgCollector = configuration.get(CLIConfigurationKeys.MESSAGE_COLLECTOR_KEY)
+                    val suppressKotlinVersionCheck = configuration.get(
+                        ComposeConfiguration.SUPPRESS_KOTLIN_VERSION_COMPATIBILITY_CHECK
+                    )
+                    if (
+                        suppressKotlinVersionCheck != null &&
+                        suppressKotlinVersionCheck != version
+                    ) {
+                        if (suppressKotlinVersionCheck == "true") {
+                            msgCollector?.report(
+                                CompilerMessageSeverity.STRONG_WARNING,
+                                " `suppressKotlinVersionCompatibilityCheck` should" +
+                                    " specify the version of Kotlin for which you want the" +
+                                    " compatibility check to be disabled. For example," +
+                                    " `suppressKotlinVersionCompatibilityCheck=$version`"
+                            )
+                        } else {
+                            msgCollector?.report(
+                                CompilerMessageSeverity.STRONG_WARNING,
+                                " `suppressKotlinVersionCompatibilityCheck` is set to a" +
+                                    " version of Kotlin ($suppressKotlinVersionCheck) that you" +
+                                    " are not using and should be set properly. (you are using" +
+                                    " Kotlin $version)"
+                            )
+                        }
+                    }
+                    if (suppressKotlinVersionCheck == KOTLIN_VERSION_EXPECTATION) {
                         msgCollector?.report(
                             CompilerMessageSeverity.STRONG_WARNING,
-                            " `suppressKotlinVersionCompatibilityCheck` should" +
-                                " specify the version of Kotlin for which you want the" +
-                                " compatibility check to be disabled. For example," +
-                                " `suppressKotlinVersionCompatibilityCheck=$version`"
-                        )
-                    } else {
-                        msgCollector?.report(
-                            CompilerMessageSeverity.STRONG_WARNING,
-                            " `suppressKotlinVersionCompatibilityCheck` is set to a" +
-                                " version of Kotlin ($suppressKotlinVersionCheck) that you" +
-                                " are not using and should be set properly. (you are using" +
-                                " Kotlin $version)"
+                            " `suppressKotlinVersionCompatibilityCheck` is set to the" +
+                                " same version of Kotlin that the Compose Compiler was already" +
+                                " expecting (Kotlin $suppressKotlinVersionCheck), and thus has" +
+                                " no effect and should be removed."
                         )
                     }
-                }
-                if (suppressKotlinVersionCheck == KOTLIN_VERSION_EXPECTATION) {
-                    msgCollector?.report(
-                        CompilerMessageSeverity.STRONG_WARNING,
-                        " `suppressKotlinVersionCompatibilityCheck` is set to the same" +
-                            " version of Kotlin that the Compose Compiler was already expecting" +
-                            " (Kotlin $suppressKotlinVersionCheck), and thus has no effect and" +
-                            " should be removed."
-                    )
-                }
-                if (suppressKotlinVersionCheck != "true" &&
-                    version != KOTLIN_VERSION_EXPECTATION &&
-                    version != suppressKotlinVersionCheck) {
-                    msgCollector?.report(
-                        CompilerMessageSeverity.ERROR,
-                        "This version (${VersionChecker.compilerVersion}) of the" +
-                            " Compose Compiler requires Kotlin version" +
-                            " $KOTLIN_VERSION_EXPECTATION but you appear to be using Kotlin" +
-                            " version $version which is not known to be compatible.  Please" +
-                            " consult the Compose-Kotlin compatibility map located at" +
-                            " https://developer.android.com" +
-                            "/jetpack/androidx/releases/compose-kotlin" +
-                            " to choose a compatible version pair (or" +
-                            " `suppressKotlinVersionCompatibilityCheck` but don't say I" +
-                            " didn't warn you!)."
-                    )
+                    if (suppressKotlinVersionCheck != "true" &&
+                        version != KOTLIN_VERSION_EXPECTATION &&
+                        version != suppressKotlinVersionCheck
+                    ) {
+                        msgCollector?.report(
+                            CompilerMessageSeverity.ERROR,
+                            "This version (${VersionChecker.compilerVersion}) of the" +
+                                " Compose Compiler requires Kotlin version" +
+                                " $KOTLIN_VERSION_EXPECTATION but you appear to be using Kotlin" +
+                                " version $version which is not known to be compatible.  Please" +
+                                " consult the Compose-Kotlin compatibility map located at" +
+                                " https://developer.android.com" +
+                                "/jetpack/androidx/releases/compose-kotlin" +
+                                " to choose a compatible version pair (or" +
+                                " `suppressKotlinVersionCompatibilityCheck` but don't say I" +
+                                " didn't warn you!)."
+                        )
 
-                    // Return without registering the Compose plugin because the registration
-                    // APIs may have changed and thus throw an exception during registration,
-                    // preventing the diagnostic from being emitted.
-                    return false
+                        // Return without registering the Compose plugin because the registration
+                        // APIs may have changed and thus throw an exception during registration,
+                        // preventing the diagnostic from being emitted.
+                        return false
+                    }
                 }
+                return true
+            } catch (t: Throwable) {
+                throw Error(
+                    "Something went wrong while checking for version compatibility" +
+                        " between the Compose Compiler and the Kotlin Compiler.  It is possible" +
+                        " that the versions are incompatible.  Please verify your kotlin version " +
+                        " and consult the Compose-Kotlin compatibility map located at" +
+                        " https://developer.android.com" +
+                        "/jetpack/androidx/releases/compose-kotlin",
+                    t
+                )
             }
-            return true
         }
 
         fun registerCommonExtensions(project: Project) {
@@ -293,10 +312,9 @@
             )
         }
 
-        fun registerIrExtension(
-            project: Project,
+        fun createComposeIrExtension(
             configuration: CompilerConfiguration
-        ) {
+        ): ComposeIrGenerationExtension {
             val liveLiteralsEnabled = configuration.getBoolean(
                 ComposeConfiguration.LIVE_LITERALS_ENABLED_KEY,
             )
@@ -328,19 +346,16 @@
                 JVMConfigurationKeys.VALIDATE_IR
             )
 
-            IrGenerationExtension.registerExtension(
-                project,
-                ComposeIrGenerationExtension(
-                    liveLiteralsEnabled = liveLiteralsEnabled,
-                    liveLiteralsV2Enabled = liveLiteralsV2Enabled,
-                    generateFunctionKeyMetaClasses = generateFunctionKeyMetaClasses,
-                    sourceInformationEnabled = sourceInformationEnabled,
-                    intrinsicRememberEnabled = intrinsicRememberEnabled,
-                    decoysEnabled = decoysEnabled,
-                    metricsDestination = metricsDestination,
-                    reportsDestination = reportsDestination,
-                    validateIr = validateIr,
-                )
+            return ComposeIrGenerationExtension(
+                liveLiteralsEnabled = liveLiteralsEnabled,
+                liveLiteralsV2Enabled = liveLiteralsV2Enabled,
+                generateFunctionKeyMetaClasses = generateFunctionKeyMetaClasses,
+                sourceInformationEnabled = sourceInformationEnabled,
+                intrinsicRememberEnabled = intrinsicRememberEnabled,
+                decoysEnabled = decoysEnabled,
+                metricsDestination = metricsDestination,
+                reportsDestination = reportsDestination,
+                validateIr = validateIr,
             )
         }
     }
diff --git a/compose/compiler/compiler-hosted/src/main/java/androidx/compose/compiler/plugins/kotlin/lower/ComposableFunctionBodyTransformer.kt b/compose/compiler/compiler-hosted/src/main/java/androidx/compose/compiler/plugins/kotlin/lower/ComposableFunctionBodyTransformer.kt
index ebd863b..a111c71 100644
--- a/compose/compiler/compiler-hosted/src/main/java/androidx/compose/compiler/plugins/kotlin/lower/ComposableFunctionBodyTransformer.kt
+++ b/compose/compiler/compiler-hosted/src/main/java/androidx/compose/compiler/plugins/kotlin/lower/ComposableFunctionBodyTransformer.kt
@@ -958,14 +958,24 @@
         val sourceInformationPreamble = mutableStatementContainer()
         val skipPreamble = mutableStatementContainer()
         val bodyPreamble = mutableStatementContainer()
+        val bodyEpilogue = mutableStatementContainer()
 
         // First generate the source information call
-        if (collectSourceInformation && !scope.isInlinedLambda) {
-            sourceInformationPreamble.statements.add(irSourceInformation(scope))
+        val isInlineLambda = scope.isInlinedLambda
+        if (collectSourceInformation) {
+            if (isInlineLambda) {
+                sourceInformationPreamble.statements.add(
+                    irSourceInformationMarkerStart(body, scope)
+                )
+                bodyEpilogue.statements.add(irSourceInformationMarkerEnd(body))
+            } else {
+                sourceInformationPreamble.statements.add(irSourceInformation(scope))
+            }
         }
 
         // we start off assuming that we *can* skip execution of the function
         var canSkipExecution = declaration.returnType.isUnit() &&
+            !isInlineLambda &&
             scope.allTrackedParams.none { stabilityOf(it.type).knownUnstable() }
 
         // if the function can never skip, or there are no parameters to test, then we
@@ -1001,7 +1011,6 @@
                 wrapWithTraceEvents(irFunctionSourceKey(), scope)
             }
         }
-
         canSkipExecution = buildPreambleStatementsAndReturnIfSkippingPossible(
             body,
             skipPreamble,
@@ -1073,6 +1082,7 @@
                     *skipPreamble.statements.toTypedArray(),
                     *bodyPreamble.statements.toTypedArray(),
                     transformed,
+                    *bodyEpilogue.statements.toTypedArray(),
                     returnVar?.let { irReturn(declaration.symbol, irGet(it)) }
                 )
             )
diff --git a/compose/foundation/foundation-lint/src/test/java/androidx/compose/foundation/lint/LazyLayoutStateReadInCompositionDetectorTest.kt b/compose/foundation/foundation-lint/src/test/java/androidx/compose/foundation/lint/LazyLayoutStateReadInCompositionDetectorTest.kt
index 0a119ff..4e448e6 100644
--- a/compose/foundation/foundation-lint/src/test/java/androidx/compose/foundation/lint/LazyLayoutStateReadInCompositionDetectorTest.kt
+++ b/compose/foundation/foundation-lint/src/test/java/androidx/compose/foundation/lint/LazyLayoutStateReadInCompositionDetectorTest.kt
@@ -17,7 +17,7 @@
 package androidx.compose.foundation.lint
 
 import androidx.compose.lint.test.Stubs
-import androidx.compose.lint.test.compiledStub
+import androidx.compose.lint.test.bytecodeStub
 import com.android.tools.lint.checks.infrastructure.LintDetectorTest
 import com.android.tools.lint.detector.api.Detector
 import com.android.tools.lint.detector.api.Issue
@@ -34,7 +34,7 @@
             LazyLayoutStateReadInCompositionDetector.FrequentlyChangedStateReadInComposition
         )
 
-    private val lazyGridStateStub = compiledStub(
+    private val lazyGridStateStub = bytecodeStub(
         filename = "LazyGridState.kt",
         filepath = "androidx/compose/foundation/lazy/grid",
         checksum = 0xd5891ae4,
@@ -100,7 +100,7 @@
         """
     )
 
-    private val lazyListStateStub = compiledStub(
+    private val lazyListStateStub = bytecodeStub(
         filename = "LazyListState.kt",
         filepath = "androidx/compose/foundation/lazy",
         checksum = 0xb9a80c68,
diff --git a/compose/foundation/foundation-lint/src/test/java/androidx/compose/foundation/lint/NonLambdaOffsetModifierDetectorTest.kt b/compose/foundation/foundation-lint/src/test/java/androidx/compose/foundation/lint/NonLambdaOffsetModifierDetectorTest.kt
index 6f0752c..d1ea423 100644
--- a/compose/foundation/foundation-lint/src/test/java/androidx/compose/foundation/lint/NonLambdaOffsetModifierDetectorTest.kt
+++ b/compose/foundation/foundation-lint/src/test/java/androidx/compose/foundation/lint/NonLambdaOffsetModifierDetectorTest.kt
@@ -17,8 +17,8 @@
 package androidx.compose.foundation.lint
 
 import androidx.compose.lint.test.Stubs
-import androidx.compose.lint.test.compiledStub
-import androidx.compose.lint.test.kotlinAndCompiledStub
+import androidx.compose.lint.test.bytecodeStub
+import androidx.compose.lint.test.kotlinAndBytecodeStub
 import com.android.tools.lint.checks.infrastructure.LintDetectorTest
 import com.android.tools.lint.checks.infrastructure.TestFile
 import com.android.tools.lint.detector.api.Detector
@@ -34,7 +34,7 @@
         "Warning: ${NonLambdaOffsetModifierDetector.ReportMainMessage} " +
             "[${NonLambdaOffsetModifierDetector.IssueId}]"
 
-    private val OffsetStub: TestFile = compiledStub(
+    private val OffsetStub: TestFile = bytecodeStub(
         filename = "Offset.kt",
         filepath = "androidx/compose/foundation/layout",
         checksum = 0xd449361a,
@@ -92,7 +92,7 @@
     )
 
     // common_typos_disable
-    private val AnotherOffsetDefinitionStub = kotlinAndCompiledStub(
+    private val AnotherOffsetDefinitionStub = kotlinAndBytecodeStub(
         filename = "InitialTestPackage.kt",
         filepath = "initial/test/pack",
         checksum = 0xd4dfae47,
@@ -186,7 +186,7 @@
     )
     // common_typos_enabled
 
-    private val DensityStub: TestFile = compiledStub(
+    private val DensityStub: TestFile = bytecodeStub(
         filename = "Density.kt",
         filepath = "androidx/compose/ui/unit",
         checksum = 0xaa534a7a,
@@ -1383,7 +1383,7 @@
 
         """
             ),
-            AnotherOffsetDefinitionStub.compiled
+            AnotherOffsetDefinitionStub.bytecode
         )
             .run()
             .expectClean()
diff --git a/compose/foundation/foundation/benchmark/src/androidTest/java/androidx/compose/foundation/benchmark/OverscrollBenchmark.kt b/compose/foundation/foundation/benchmark/src/androidTest/java/androidx/compose/foundation/benchmark/OverscrollBenchmark.kt
index 40ca1ee..740bf49 100644
--- a/compose/foundation/foundation/benchmark/src/androidTest/java/androidx/compose/foundation/benchmark/OverscrollBenchmark.kt
+++ b/compose/foundation/foundation/benchmark/src/androidTest/java/androidx/compose/foundation/benchmark/OverscrollBenchmark.kt
@@ -54,7 +54,6 @@
 import androidx.compose.ui.unit.dp
 import androidx.test.ext.junit.runners.AndroidJUnit4
 import androidx.test.filters.LargeTest
-import org.junit.Ignore
 import org.junit.Rule
 import org.junit.Test
 import org.junit.runner.RunWith
@@ -87,19 +86,16 @@
         benchmarkRule.benchmarkFirstDraw(overscrollTestCase)
     }
 
-    @Ignore // b/265351382
     @Test
     fun overscroll_measure() {
         benchmarkRule.toggleStateBenchmarkMeasure(overscrollTestCase, false)
     }
 
-    @Ignore // b/265351382
     @Test
     fun overscroll_layout() {
         benchmarkRule.toggleStateBenchmarkLayout(overscrollTestCase, false)
     }
 
-    @Ignore // b/265351382
     @Test
     fun overscroll_draw() {
         benchmarkRule.toggleStateBenchmarkDraw(overscrollTestCase, false)
@@ -165,7 +161,7 @@
             motionEventHelper.sendEvent(MotionEvent.ACTION_MOVE, Offset(x = 0f, y = height / 8f))
             showingOverscroll = true
         } else {
-            motionEventHelper.sendEvent(MotionEvent.ACTION_UP, Offset.Zero)
+            motionEventHelper.sendEvent(MotionEvent.ACTION_UP, Offset.Zero, 1000L)
             showingOverscroll = false
         }
     }
diff --git a/compose/foundation/foundation/benchmark/src/androidTest/java/androidx/compose/foundation/benchmark/lazy/LazyBenchmarkCommon.kt b/compose/foundation/foundation/benchmark/src/androidTest/java/androidx/compose/foundation/benchmark/lazy/LazyBenchmarkCommon.kt
index 617ba28..f342fc4 100644
--- a/compose/foundation/foundation/benchmark/src/androidTest/java/androidx/compose/foundation/benchmark/lazy/LazyBenchmarkCommon.kt
+++ b/compose/foundation/foundation/benchmark/src/androidTest/java/androidx/compose/foundation/benchmark/lazy/LazyBenchmarkCommon.kt
@@ -43,9 +43,10 @@
 
     fun sendEvent(
         action: Int,
-        delta: Offset
+        delta: Offset,
+        timeDelta: Long = 10L
     ) {
-        time += 10L
+        time += timeDelta
 
         val coord = delta + (lastCoord ?: Offset.Zero)
 
diff --git a/compose/foundation/foundation/integration-tests/foundation-demos/src/main/java/androidx/compose/foundation/demos/text/MemoryAllocs.kt b/compose/foundation/foundation/integration-tests/foundation-demos/src/main/java/androidx/compose/foundation/demos/text/MemoryAllocs.kt
index bfbb956..b191349a 100644
--- a/compose/foundation/foundation/integration-tests/foundation-demos/src/main/java/androidx/compose/foundation/demos/text/MemoryAllocs.kt
+++ b/compose/foundation/foundation/integration-tests/foundation-demos/src/main/java/androidx/compose/foundation/demos/text/MemoryAllocs.kt
@@ -16,12 +16,21 @@
 
 package androidx.compose.foundation.demos.text
 
+import androidx.compose.foundation.background
 import androidx.compose.foundation.layout.Column
+import androidx.compose.foundation.layout.fillMaxWidth
+import androidx.compose.foundation.layout.padding
+import androidx.compose.material.Divider
 import androidx.compose.material.Text
 import androidx.compose.runtime.Composable
 import androidx.compose.runtime.State
 import androidx.compose.runtime.produceState
 import androidx.compose.runtime.withFrameMillis
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.graphics.Color
+import androidx.compose.ui.text.font.FontFamily
+import androidx.compose.ui.unit.dp
+import androidx.compose.ui.unit.sp
 
 /**
  * These demos are for using the memory profiler to observe initial compo and recompo memory
@@ -32,8 +41,12 @@
 @Composable
 fun MemoryAllocsSetText() {
     Column {
-        Text("Run in memory profiler to emulate setting text value when observable loads")
-        Text("This is designed to be used in the Android Studio memory profiler")
+        Preamble("""
+            @Composable
+            fun SetText(text: State<String>) {
+                Text(text.value)
+            }""".trimIndent()
+        )
         SetText(textToggler())
     }
 }
@@ -47,13 +60,37 @@
 @Composable
 fun MemoryAllocsIfNotEmptyText() {
     Column {
-        Text("Run in memory profiler to emulate calling Text after an observable loads")
-        Text("This is designed to be used in the Android Studio memory profiler")
+        Preamble("""
+            @Composable
+            fun IfNotEmptyText(text: State<String>) {
+                if (text.value.isNotEmpty()) {
+                    Text(text.value)
+                }
+            }""".trimIndent()
+        )
         IfNotEmptyText(textToggler())
     }
 }
 
 @Composable
+fun Preamble(sourceCode: String) {
+    Text("Run in memory profiler to emulate text behavior during observable loads")
+    Text(text = sourceCode,
+        modifier = Modifier
+            .fillMaxWidth()
+            .background(Color(220, 230, 240)),
+        fontFamily = FontFamily.Monospace,
+        color = Color(41, 17, 27),
+        fontSize = 10.sp
+    )
+    Divider(
+        Modifier
+            .fillMaxWidth()
+            .padding(vertical = 8.dp))
+    Text("\uD83D\uDC47 running here \uD83D\uDC47")
+}
+
+@Composable
 fun IfNotEmptyText(text: State<String>) {
     if (text.value.isNotEmpty()) {
         Text(text.value)
diff --git a/compose/foundation/foundation/integration-tests/foundation-demos/src/main/java/androidx/compose/foundation/demos/text/TextDemos.kt b/compose/foundation/foundation/integration-tests/foundation-demos/src/main/java/androidx/compose/foundation/demos/text/TextDemos.kt
index 62e4dbf..258991f 100644
--- a/compose/foundation/foundation/integration-tests/foundation-demos/src/main/java/androidx/compose/foundation/demos/text/TextDemos.kt
+++ b/compose/foundation/foundation/integration-tests/foundation-demos/src/main/java/androidx/compose/foundation/demos/text/TextDemos.kt
@@ -117,10 +117,10 @@
             )
         ),
         DemoCategory(
-            "⚠️️ Memory benchmark ⚠️️",
+            "\uD83D\uDD75️️️ Memory allocs",
             listOf(
-                ComposableDemo("SetText") { MemoryAllocsSetText() },
-                ComposableDemo("IfNotEmptyText") { MemoryAllocsIfNotEmptyText() }
+                ComposableDemo("\uD83D\uDD75️ SetText") { MemoryAllocsSetText() },
+                ComposableDemo("\uD83D\uDD75️ IfNotEmptyText") { MemoryAllocsIfNotEmptyText() }
             )
         )
     )
diff --git a/compose/foundation/foundation/src/androidAndroidTest/kotlin/androidx/compose/foundation/lazy/grid/LazyGridPinnableContainerTest.kt b/compose/foundation/foundation/src/androidAndroidTest/kotlin/androidx/compose/foundation/lazy/grid/LazyGridPinnableContainerTest.kt
index 91e6a5b..a2ce246 100644
--- a/compose/foundation/foundation/src/androidAndroidTest/kotlin/androidx/compose/foundation/lazy/grid/LazyGridPinnableContainerTest.kt
+++ b/compose/foundation/foundation/src/androidAndroidTest/kotlin/androidx/compose/foundation/lazy/grid/LazyGridPinnableContainerTest.kt
@@ -255,7 +255,7 @@
         }
 
         rule.runOnIdle {
-            handle.unpin()
+            handle.release()
         }
 
         rule.waitUntil {
@@ -574,7 +574,7 @@
         while (handles.isNotEmpty()) {
             rule.runOnIdle {
                 assertThat(composed).contains(1)
-                handles.removeFirst().unpin()
+                handles.removeFirst().release()
             }
         }
 
@@ -611,7 +611,7 @@
 
         rule.runOnIdle {
             assertThat(parentPinned).isTrue()
-            handle.unpin()
+            handle.release()
         }
 
         rule.runOnIdle {
diff --git a/compose/foundation/foundation/src/androidAndroidTest/kotlin/androidx/compose/foundation/lazy/grid/LazyScrollTest.kt b/compose/foundation/foundation/src/androidAndroidTest/kotlin/androidx/compose/foundation/lazy/grid/LazyScrollTest.kt
index 5d6408a..3bf22a4 100644
--- a/compose/foundation/foundation/src/androidAndroidTest/kotlin/androidx/compose/foundation/lazy/grid/LazyScrollTest.kt
+++ b/compose/foundation/foundation/src/androidAndroidTest/kotlin/androidx/compose/foundation/lazy/grid/LazyScrollTest.kt
@@ -244,6 +244,25 @@
     }
 
     @Test
+    fun animateScrollToItemWithOffsetLargerThanItemSize_forward() = runBlocking {
+        withContext(Dispatchers.Main + AutoTestFrameClock()) {
+            state.animateScrollToItem(10, -itemSizePx * 3)
+        }
+        assertThat(state.firstVisibleItemIndex).isEqualTo(4)
+        assertThat(state.firstVisibleItemScrollOffset).isEqualTo(0)
+    }
+
+    @Test
+    fun animateScrollToItemWithOffsetLargerThanItemSize_backward() = runBlocking {
+        withContext(Dispatchers.Main + AutoTestFrameClock()) {
+            state.scrollToItem(10)
+            state.animateScrollToItem(0, itemSizePx * 3)
+        }
+        assertThat(state.firstVisibleItemIndex).isEqualTo(6)
+        assertThat(state.firstVisibleItemScrollOffset).isEqualTo(0)
+    }
+
+    @Test
     fun canScrollForward() = runBlocking {
         assertThat(state.firstVisibleItemScrollOffset).isEqualTo(0)
         assertThat(state.canScrollForward).isTrue()
diff --git a/compose/foundation/foundation/src/androidAndroidTest/kotlin/androidx/compose/foundation/lazy/list/LazyListPinnableContainerTest.kt b/compose/foundation/foundation/src/androidAndroidTest/kotlin/androidx/compose/foundation/lazy/list/LazyListPinnableContainerTest.kt
index cb725cf..a743ac3c 100644
--- a/compose/foundation/foundation/src/androidAndroidTest/kotlin/androidx/compose/foundation/lazy/list/LazyListPinnableContainerTest.kt
+++ b/compose/foundation/foundation/src/androidAndroidTest/kotlin/androidx/compose/foundation/lazy/list/LazyListPinnableContainerTest.kt
@@ -241,7 +241,7 @@
         }
 
         rule.runOnIdle {
-            handle.unpin()
+            handle.release()
         }
 
         rule.waitUntil {
@@ -532,7 +532,7 @@
         while (handles.isNotEmpty()) {
             rule.runOnIdle {
                 assertThat(composed).contains(1)
-                handles.removeFirst().unpin()
+                handles.removeFirst().release()
             }
         }
 
@@ -569,7 +569,7 @@
 
         rule.runOnIdle {
             assertThat(parentPinned).isTrue()
-            handle.unpin()
+            handle.release()
         }
 
         rule.runOnIdle {
diff --git a/compose/foundation/foundation/src/androidAndroidTest/kotlin/androidx/compose/foundation/lazy/list/LazyScrollTest.kt b/compose/foundation/foundation/src/androidAndroidTest/kotlin/androidx/compose/foundation/lazy/list/LazyScrollTest.kt
index e0fdf831..8d2a237 100644
--- a/compose/foundation/foundation/src/androidAndroidTest/kotlin/androidx/compose/foundation/lazy/list/LazyScrollTest.kt
+++ b/compose/foundation/foundation/src/androidAndroidTest/kotlin/androidx/compose/foundation/lazy/list/LazyScrollTest.kt
@@ -244,6 +244,25 @@
     }
 
     @Test
+    fun animateScrollToItemWithOffsetLargerThanItemSize_forward() = runBlocking {
+        withContext(Dispatchers.Main + AutoTestFrameClock()) {
+            state.animateScrollToItem(10, -itemSizePx * 3)
+        }
+        assertThat(state.firstVisibleItemIndex).isEqualTo(7)
+        assertThat(state.firstVisibleItemScrollOffset).isEqualTo(0)
+    }
+
+    @Test
+    fun animateScrollToItemWithOffsetLargerThanItemSize_backward() = runBlocking {
+        withContext(Dispatchers.Main + AutoTestFrameClock()) {
+            state.scrollToItem(10)
+            state.animateScrollToItem(0, itemSizePx * 3)
+        }
+        assertThat(state.firstVisibleItemIndex).isEqualTo(3)
+        assertThat(state.firstVisibleItemScrollOffset).isEqualTo(0)
+    }
+
+    @Test
     fun canScrollForward() = runBlocking {
         assertThat(state.firstVisibleItemScrollOffset).isEqualTo(0)
         assertThat(state.canScrollForward).isTrue()
diff --git a/compose/foundation/foundation/src/androidAndroidTest/kotlin/androidx/compose/foundation/lazy/staggeredgrid/LazyStaggeredGridAnimatedScrollTest.kt b/compose/foundation/foundation/src/androidAndroidTest/kotlin/androidx/compose/foundation/lazy/staggeredgrid/LazyStaggeredGridAnimatedScrollTest.kt
index dbb0f0c..627f6fb 100644
--- a/compose/foundation/foundation/src/androidAndroidTest/kotlin/androidx/compose/foundation/lazy/staggeredgrid/LazyStaggeredGridAnimatedScrollTest.kt
+++ b/compose/foundation/foundation/src/androidAndroidTest/kotlin/androidx/compose/foundation/lazy/staggeredgrid/LazyStaggeredGridAnimatedScrollTest.kt
@@ -188,6 +188,26 @@
         assertSpringAnimation(fromIndex = 10, fromOffset = 10, toIndex = 0, toOffset = 10)
     }
 
+    @Test
+    fun animateScrollToItemWithOffsetLargerThanItemSize_forward() {
+        runBlocking(Dispatchers.Main + AutoTestFrameClock()) {
+            state.animateScrollToItem(20, -itemSizePx * 3)
+        }
+        rule.waitForIdle()
+        assertThat(state.firstVisibleItemIndex).isEqualTo(14)
+        assertThat(state.firstVisibleItemScrollOffset).isEqualTo(0)
+    }
+
+    @Test
+    fun animateScrollToItemWithOffsetLargerThanItemSize_backward() = runBlocking {
+        withContext(Dispatchers.Main + AutoTestFrameClock()) {
+            state.scrollToItem(20)
+            state.animateScrollToItem(0, itemSizePx * 3)
+        }
+        assertThat(state.firstVisibleItemIndex).isEqualTo(6)
+        assertThat(state.firstVisibleItemScrollOffset).isEqualTo(0)
+    }
+
     private fun assertSpringAnimation(
         toIndex: Int,
         toOffset: Int = 0,
diff --git a/compose/foundation/foundation/src/androidAndroidTest/kotlin/androidx/compose/foundation/lazy/staggeredgrid/LazyStaggeredGridPinnableContainerTest.kt b/compose/foundation/foundation/src/androidAndroidTest/kotlin/androidx/compose/foundation/lazy/staggeredgrid/LazyStaggeredGridPinnableContainerTest.kt
index 7d688a4..5796438 100644
--- a/compose/foundation/foundation/src/androidAndroidTest/kotlin/androidx/compose/foundation/lazy/staggeredgrid/LazyStaggeredGridPinnableContainerTest.kt
+++ b/compose/foundation/foundation/src/androidAndroidTest/kotlin/androidx/compose/foundation/lazy/staggeredgrid/LazyStaggeredGridPinnableContainerTest.kt
@@ -257,7 +257,7 @@
         }
 
         rule.runOnIdle {
-            handle.unpin()
+            handle.release()
         }
 
         rule.waitUntil {
@@ -576,7 +576,7 @@
         while (handles.isNotEmpty()) {
             rule.runOnIdle {
                 assertThat(composed).contains(1)
-                handles.removeFirst().unpin()
+                handles.removeFirst().release()
             }
         }
 
@@ -613,7 +613,7 @@
 
         rule.runOnIdle {
             assertThat(parentPinned).isTrue()
-            handle.unpin()
+            handle.release()
         }
 
         rule.runOnIdle {
diff --git a/compose/foundation/foundation/src/androidAndroidTest/kotlin/androidx/compose/foundation/pager/BasePagerTest.kt b/compose/foundation/foundation/src/androidAndroidTest/kotlin/androidx/compose/foundation/pager/BasePagerTest.kt
index 5fc70d3..b0a59e3 100644
--- a/compose/foundation/foundation/src/androidAndroidTest/kotlin/androidx/compose/foundation/pager/BasePagerTest.kt
+++ b/compose/foundation/foundation/src/androidAndroidTest/kotlin/androidx/compose/foundation/pager/BasePagerTest.kt
@@ -114,7 +114,7 @@
     internal fun createPager(
         state: PagerState,
         modifier: Modifier = Modifier,
-        pagerCount: () -> Int = { DefaultPageCount },
+        pageCount: () -> Int = { DefaultPageCount },
         offscreenPageLimit: Int = 0,
         pageSize: PageSize = PageSize.Fill,
         userScrollEnabled: Boolean = true,
@@ -142,7 +142,7 @@
                         .nestedScroll(nestedScrollConnection)
                 ) {
                     HorizontalOrVerticalPager(
-                        pageCount = pagerCount(),
+                        pageCount = pageCount(),
                         state = state,
                         beyondBoundsPageCount = offscreenPageLimit,
                         modifier = modifier
diff --git a/compose/foundation/foundation/src/androidAndroidTest/kotlin/androidx/compose/foundation/pager/EmptyPagerTests.kt b/compose/foundation/foundation/src/androidAndroidTest/kotlin/androidx/compose/foundation/pager/EmptyPagerTests.kt
index 776098c..874b208 100644
--- a/compose/foundation/foundation/src/androidAndroidTest/kotlin/androidx/compose/foundation/pager/EmptyPagerTests.kt
+++ b/compose/foundation/foundation/src/androidAndroidTest/kotlin/androidx/compose/foundation/pager/EmptyPagerTests.kt
@@ -36,7 +36,7 @@
         val state = PagerState()
 
         // Act
-        createPager(state = state, modifier = Modifier.fillMaxSize(), pagerCount = { 0 })
+        createPager(state = state, modifier = Modifier.fillMaxSize(), pageCount = { 0 })
 
         // Assert
         rule.onNodeWithTag("0").assertDoesNotExist()
diff --git a/compose/foundation/foundation/src/androidAndroidTest/kotlin/androidx/compose/foundation/pager/PagerScrollingTest.kt b/compose/foundation/foundation/src/androidAndroidTest/kotlin/androidx/compose/foundation/pager/PagerScrollingTest.kt
index ab438c1..4ddc0c3 100644
--- a/compose/foundation/foundation/src/androidAndroidTest/kotlin/androidx/compose/foundation/pager/PagerScrollingTest.kt
+++ b/compose/foundation/foundation/src/androidAndroidTest/kotlin/androidx/compose/foundation/pager/PagerScrollingTest.kt
@@ -23,8 +23,10 @@
 import androidx.compose.ui.test.assertIsDisplayed
 import androidx.compose.ui.test.onNodeWithTag
 import androidx.compose.ui.test.performTouchInput
+import androidx.compose.ui.unit.dp
 import androidx.test.filters.LargeTest
 import com.google.common.truth.Truth.assertThat
+import kotlinx.coroutines.runBlocking
 import org.junit.Test
 import org.junit.runner.RunWith
 import org.junit.runners.Parameterized
@@ -175,6 +177,41 @@
     }
 
     @Test
+    fun swipeWithHighVelocity_overHalfPage_shouldGoToNextPage() {
+        // Arrange
+        val state = PagerState(5)
+        createPager(state = state, modifier = Modifier.fillMaxSize())
+        // make sure the scroll distance is not enough to go to next page
+        val delta = pagerSize * 0.8f * scrollForwardSign
+
+        // Act - forward
+        onPager().performTouchInput {
+            swipeWithVelocityAcrossMainAxis(
+                with(rule.density) { 1.1f * MinFlingVelocityDp.toPx() },
+                delta
+            )
+        }
+        rule.waitForIdle()
+
+        // Assert
+        rule.onNodeWithTag("6").assertIsDisplayed()
+        confirmPageIsInCorrectPosition(6)
+
+        // Act - backward
+        onPager().performTouchInput {
+            swipeWithVelocityAcrossMainAxis(
+                with(rule.density) { 1.1f * MinFlingVelocityDp.toPx() },
+                delta * -1
+            )
+        }
+        rule.waitForIdle()
+
+        // Assert
+        rule.onNodeWithTag("5").assertIsDisplayed()
+        confirmPageIsInCorrectPosition(5)
+    }
+
+    @Test
     fun scrollWithoutVelocity_shouldSettlingInClosestPage() {
         // Arrange
         val state = PagerState(5)
@@ -205,6 +242,106 @@
         confirmPageIsInCorrectPosition(state.currentPage)
     }
 
+    @Test
+    fun scrollWithSameVelocity_shouldYieldSameResult_forward() {
+        // Arrange
+        var initialPage = 1
+        val state = PagerState(initialPage)
+        createPager(
+            pageSize = PageSize.Fixed(200.dp),
+            state = state,
+            modifier = Modifier.fillMaxSize(),
+            pageCount = { 100 },
+            snappingPage = PagerSnapDistance.atMost(3)
+        )
+        // This will scroll 0.5 page before flinging
+        val delta = pagerSize * 0.5f * scrollForwardSign
+
+        // Act - forward
+        onPager().performTouchInput {
+            swipeWithVelocityAcrossMainAxis(2000f, delta)
+        }
+        rule.waitForIdle()
+
+        val pageDisplacement = state.currentPage - initialPage
+
+        // Repeat starting from different places
+        // reset
+        initialPage = 10
+        rule.runOnIdle {
+            runBlocking { state.scrollToPage(initialPage) }
+        }
+
+        onPager().performTouchInput {
+            swipeWithVelocityAcrossMainAxis(2000f, delta)
+        }
+        rule.waitForIdle()
+
+        assertThat(state.currentPage - initialPage).isEqualTo(pageDisplacement)
+
+        initialPage = 50
+        rule.runOnIdle {
+            runBlocking { state.scrollToPage(initialPage) }
+        }
+
+        onPager().performTouchInput {
+            swipeWithVelocityAcrossMainAxis(2000f, delta)
+        }
+        rule.waitForIdle()
+
+        assertThat(state.currentPage - initialPage).isEqualTo(pageDisplacement)
+    }
+
+    @Test
+    fun scrollWithSameVelocity_shouldYieldSameResult_backward() {
+        // Arrange
+        var initialPage = 90
+        val state = PagerState(initialPage)
+        createPager(
+            pageSize = PageSize.Fixed(200.dp),
+            state = state,
+            modifier = Modifier.fillMaxSize(),
+            pageCount = { 100 },
+            snappingPage = PagerSnapDistance.atMost(3)
+        )
+        // This will scroll 0.5 page before flinging
+        val delta = pagerSize * -0.5f * scrollForwardSign
+
+        // Act - forward
+        onPager().performTouchInput {
+            swipeWithVelocityAcrossMainAxis(2000f, delta)
+        }
+        rule.waitForIdle()
+
+        val pageDisplacement = state.currentPage - initialPage
+
+        // Repeat starting from different places
+        // reset
+        initialPage = 70
+        rule.runOnIdle {
+            runBlocking { state.scrollToPage(initialPage) }
+        }
+
+        onPager().performTouchInput {
+            swipeWithVelocityAcrossMainAxis(2000f, delta)
+        }
+        rule.waitForIdle()
+
+        assertThat(state.currentPage - initialPage).isEqualTo(pageDisplacement)
+
+        initialPage = 30
+        rule.runOnIdle {
+            runBlocking { state.scrollToPage(initialPage) }
+        }
+
+        onPager().performTouchInput {
+            swipeWithVelocityAcrossMainAxis(2000f, delta)
+        }
+        rule.waitForIdle()
+
+        assertThat(state.currentPage - initialPage).isEqualTo(pageDisplacement)
+    }
+
     companion object {
         @JvmStatic
         @Parameterized.Parameters(name = "{0}")
diff --git a/compose/foundation/foundation/src/androidAndroidTest/kotlin/androidx/compose/foundation/pager/PagerStateTest.kt b/compose/foundation/foundation/src/androidAndroidTest/kotlin/androidx/compose/foundation/pager/PagerStateTest.kt
index 2fec561..aa0fdfe 100644
--- a/compose/foundation/foundation/src/androidAndroidTest/kotlin/androidx/compose/foundation/pager/PagerStateTest.kt
+++ b/compose/foundation/foundation/src/androidAndroidTest/kotlin/androidx/compose/foundation/pager/PagerStateTest.kt
@@ -404,6 +404,55 @@
     }
 
     @Test
+    fun targetPage_performScrollBelowThreshold_shouldNotShowNextPage() {
+        // Arrange
+        val state = PagerState()
+        createPager(
+            state = state,
+            modifier = Modifier.fillMaxSize(),
+            snappingPage = PagerSnapDistance.atMost(3)
+        )
+        rule.runOnIdle { assertThat(state.targetPage).isEqualTo(state.currentPage) }
+
+        rule.mainClock.autoAdvance = false
+        // Act
+        // Moving less than threshold
+        val forwardDelta =
+            scrollForwardSign.toFloat() * with(rule.density) { DefaultPositionThreshold.toPx() / 2 }
+
+        var previousTargetPage = state.targetPage
+
+        onPager().performTouchInput {
+            down(layoutStart)
+            moveBy(Offset(forwardDelta, forwardDelta))
+        }
+
+        // Assert
+        assertThat(state.targetPage).isEqualTo(previousTargetPage)
+
+        // Reset
+        rule.mainClock.autoAdvance = true
+        onPager().performTouchInput { up() }
+        rule.runOnIdle { assertThat(state.targetPage).isEqualTo(state.currentPage) }
+
+        // Act
+        // Moving more than threshold
+        val backwardDelta = scrollForwardSign.toFloat() * with(rule.density) {
+                -DefaultPositionThreshold.toPx() / 2
+        }
+
+        previousTargetPage = state.targetPage
+
+        onPager().performTouchInput {
+            down(layoutStart)
+            moveBy(Offset(backwardDelta, backwardDelta))
+        }
+
+        // Assert
+        assertThat(state.targetPage).isEqualTo(previousTargetPage)
+    }
+
+    @Test
     fun targetPage_performScroll_shouldShowNextPage() {
         // Arrange
         val state = PagerState()
@@ -473,9 +522,9 @@
             swipeWithVelocityAcrossMainAxis(20000f, forwardDelta)
         }
         rule.mainClock.advanceTimeUntil { state.targetPage != previousTarget }
-
+        var flingOriginIndex = state.firstVisiblePage?.index ?: 0
         // Assert
-        assertThat(state.targetPage).isEqualTo(state.currentPage + 3)
+        assertThat(state.targetPage).isEqualTo(flingOriginIndex + 3)
         assertThat(state.targetPage).isNotEqualTo(state.currentPage)
 
         rule.mainClock.autoAdvance = true
@@ -491,7 +540,8 @@
         rule.mainClock.advanceTimeUntil { state.targetPage != previousTarget }
 
         // Assert
-        assertThat(state.targetPage).isEqualTo(state.currentPage - 3)
+        flingOriginIndex = (state.firstVisiblePage?.index ?: 0) + 1
+        assertThat(state.targetPage).isEqualTo(flingOriginIndex - 3)
         assertThat(state.targetPage).isNotEqualTo(state.currentPage)
 
         rule.mainClock.autoAdvance = true
diff --git a/compose/foundation/foundation/src/androidAndroidTest/kotlin/androidx/compose/foundation/pager/PagerTest.kt b/compose/foundation/foundation/src/androidAndroidTest/kotlin/androidx/compose/foundation/pager/PagerTest.kt
index 941a9a7..6024528 100644
--- a/compose/foundation/foundation/src/androidAndroidTest/kotlin/androidx/compose/foundation/pager/PagerTest.kt
+++ b/compose/foundation/foundation/src/androidAndroidTest/kotlin/androidx/compose/foundation/pager/PagerTest.kt
@@ -135,7 +135,7 @@
         createPager(
             state = state,
             modifier = Modifier.fillMaxSize(),
-            pagerCount = { pageCount.value }
+            pageCount = { pageCount.value }
         )
 
         rule.onNodeWithTag("3").assertDoesNotExist()
diff --git a/compose/foundation/foundation/src/androidAndroidTest/kotlin/androidx/compose/foundation/text/selection/SelectionContainerTest.kt b/compose/foundation/foundation/src/androidAndroidTest/kotlin/androidx/compose/foundation/text/selection/SelectionContainerTest.kt
index 64469cd..03d1910 100644
--- a/compose/foundation/foundation/src/androidAndroidTest/kotlin/androidx/compose/foundation/text/selection/SelectionContainerTest.kt
+++ b/compose/foundation/foundation/src/androidAndroidTest/kotlin/androidx/compose/foundation/text/selection/SelectionContainerTest.kt
@@ -26,12 +26,14 @@
 import androidx.compose.runtime.CompositionLocalProvider
 import androidx.compose.runtime.mutableStateOf
 import androidx.compose.runtime.remember
+import androidx.compose.ui.ExperimentalComposeUiApi
 import androidx.compose.ui.Modifier
 import androidx.compose.ui.composed
 import androidx.compose.ui.geometry.Offset
 import androidx.compose.ui.geometry.Rect
 import androidx.compose.ui.hapticfeedback.HapticFeedback
 import androidx.compose.ui.hapticfeedback.HapticFeedbackType
+import androidx.compose.ui.input.key.Key
 import androidx.compose.ui.input.pointer.PointerEvent
 import androidx.compose.ui.input.pointer.PointerEventPass
 import androidx.compose.ui.input.pointer.PointerInputChange
@@ -40,10 +42,13 @@
 import androidx.compose.ui.input.pointer.changedToUp
 import androidx.compose.ui.layout.Layout
 import androidx.compose.ui.layout.onGloballyPositioned
+import androidx.compose.ui.platform.ClipboardManager
+import androidx.compose.ui.platform.LocalClipboardManager
 import androidx.compose.ui.platform.LocalHapticFeedback
 import androidx.compose.ui.platform.LocalLayoutDirection
 import androidx.compose.ui.platform.testTag
 import androidx.compose.ui.semantics.SemanticsActions
+import androidx.compose.ui.test.ExperimentalTestApi
 import androidx.compose.ui.test.SemanticsNodeInteraction
 import androidx.compose.ui.test.SemanticsNodeInteractionsProvider
 import androidx.compose.ui.test.TouchInjectionScope
@@ -57,6 +62,7 @@
 import androidx.compose.ui.test.longClick
 import androidx.compose.ui.test.onNodeWithTag
 import androidx.compose.ui.test.onRoot
+import androidx.compose.ui.test.performKeyInput
 import androidx.compose.ui.test.performSemanticsAction
 import androidx.compose.ui.test.performTouchInput
 import androidx.compose.ui.text.AnnotatedString
@@ -84,7 +90,6 @@
 import org.junit.Test
 import org.junit.runner.RunWith
 
-@Suppress("DEPRECATION")
 @LargeTest
 @RunWith(AndroidJUnit4::class)
 class SelectionContainerTest {
@@ -396,6 +401,38 @@
         assertAnchorInfo(selection.value?.end, offset = 4, selectableId = 2)
     }
 
+    @Test
+    @OptIn(ExperimentalTestApi::class, ExperimentalComposeUiApi::class)
+    fun selection_doesCopy_whenCopyKeyEventSent() {
+        lateinit var clipboardManager: ClipboardManager
+        createSelectionContainer {
+            clipboardManager = LocalClipboardManager.current
+            clipboardManager.setText(AnnotatedString("Clipboard content at start of test."))
+            Column {
+                BasicText(
+                    text = "ExpectedText",
+                    modifier = Modifier
+                        .fillMaxWidth()
+                        .testTag(tag1),
+                )
+            }
+        }
+
+        startSelection(tag1)
+
+        rule.onNodeWithTag(tag1)
+            .performKeyInput {
+                keyDown(Key.CtrlLeft)
+                keyDown(Key.C)
+                keyUp(Key.C)
+                keyUp(Key.CtrlLeft)
+            }
+
+        rule.runOnIdle {
+            assertThat(clipboardManager.getText()?.text).isEqualTo("ExpectedText")
+        }
+    }
+
     private fun startSelection(
         tag: String,
         offset: Int = 0
diff --git a/compose/foundation/foundation/src/androidAndroidTest/kotlin/androidx/compose/foundation/textfield/TextFieldSelectionTest.kt b/compose/foundation/foundation/src/androidAndroidTest/kotlin/androidx/compose/foundation/textfield/TextFieldSelectionTest.kt
index 4130a1c..5abb731 100644
--- a/compose/foundation/foundation/src/androidAndroidTest/kotlin/androidx/compose/foundation/textfield/TextFieldSelectionTest.kt
+++ b/compose/foundation/foundation/src/androidAndroidTest/kotlin/androidx/compose/foundation/textfield/TextFieldSelectionTest.kt
@@ -206,6 +206,7 @@
         assertThat(cursorPositions).isEqualTo(expectedCursorPositions)
     }
 
+    @Ignore // b/265023621
     @Test
     fun textField_extendsSelection_toRight() {
         textField_extendsSelection(
diff --git a/compose/foundation/foundation/src/androidMain/kotlin/androidx/compose/foundation/text/selection/SelectionManager.android.kt b/compose/foundation/foundation/src/androidMain/kotlin/androidx/compose/foundation/text/selection/SelectionManager.android.kt
index 53d5ea3..fb8c947 100644
--- a/compose/foundation/foundation/src/androidMain/kotlin/androidx/compose/foundation/text/selection/SelectionManager.android.kt
+++ b/compose/foundation/foundation/src/androidMain/kotlin/androidx/compose/foundation/text/selection/SelectionManager.android.kt
@@ -20,6 +20,8 @@
 import androidx.compose.foundation.ExperimentalFoundationApi
 import androidx.compose.foundation.MagnifierStyle
 import androidx.compose.foundation.magnifier
+import androidx.compose.foundation.text.KeyCommand
+import androidx.compose.foundation.text.platformDefaultKeyMapping
 import androidx.compose.runtime.getValue
 import androidx.compose.runtime.mutableStateOf
 import androidx.compose.runtime.remember
@@ -30,8 +32,8 @@
 import androidx.compose.ui.platform.LocalDensity
 import androidx.compose.ui.unit.IntSize
 
-// TODO(b/139322105) Implement for Android when hardware keyboard is implemented
-internal actual fun isCopyKeyEvent(keyEvent: KeyEvent) = false
+internal actual fun isCopyKeyEvent(keyEvent: KeyEvent) =
+    platformDefaultKeyMapping.map(keyEvent) == KeyCommand.COPY
 
 // We use composed{} to read a local, but don't provide inspector info because the underlying
 // magnifier modifier provides more meaningful inspector info.
diff --git a/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/Focusable.kt b/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/Focusable.kt
index 554c6ae..e3f6e78 100644
--- a/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/Focusable.kt
+++ b/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/Focusable.kt
@@ -121,7 +121,7 @@
                 pinHandle = pinnableContainer?.pin()
             }
             onDispose {
-                pinHandle?.unpin()
+                pinHandle?.release()
                 pinHandle = null
             }
         }
@@ -154,7 +154,7 @@
                         bringIntoViewRequester.bringIntoView()
                     }
                 } else {
-                    pinHandle?.unpin()
+                    pinHandle?.release()
                     pinHandle = null
                     scope.launch {
                         focusedInteraction.value?.let { oldValue ->
diff --git a/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/gestures/snapping/SnapFlingBehavior.kt b/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/gestures/snapping/SnapFlingBehavior.kt
index 222e742..024184a 100644
--- a/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/gestures/snapping/SnapFlingBehavior.kt
+++ b/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/gestures/snapping/SnapFlingBehavior.kt
@@ -353,6 +353,8 @@
         density.calculateSnappingOffsetBounds()
     }
 
+    debugLog { "Proposed Bounds: Lower=$lowerBound Upper=$upperBound" }
+
     val finalDistance = when (sign(velocity)) {
         0f -> {
             if (abs(upperBound) <= abs(lowerBound)) {
@@ -410,12 +412,17 @@
             val finalDelta = finalValue - previousValue
             consumeDelta(finalDelta)
             cancelAnimation()
+            previousValue = finalValue
         } else {
             val delta = value - previousValue
             consumeDelta(delta)
             previousValue = value
         }
     }
+
+    debugLog {
+        "Decay Animation: Proposed Offset=$targetOffset Achieved Offset=$previousValue"
+    }
     return AnimationResult(
         targetOffset - previousValue,
         animationState
diff --git a/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/lazy/LazyListAnimateScrollScope.kt b/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/lazy/LazyListAnimateScrollScope.kt
index 551ebc0..c048b9b 100644
--- a/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/lazy/LazyListAnimateScrollScope.kt
+++ b/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/lazy/LazyListAnimateScrollScope.kt
@@ -21,6 +21,7 @@
 import androidx.compose.ui.unit.Density
 import androidx.compose.ui.util.fastFirstOrNull
 import androidx.compose.ui.util.fastSumBy
+import kotlin.math.abs
 
 internal class LazyListAnimateScrollScope(
     private val state: LazyListState
@@ -52,8 +53,10 @@
         val visibleItems = state.layoutInfo.visibleItemsInfo
         val averageSize = visibleItems.fastSumBy { it.size } / visibleItems.size
         val indexesDiff = index - firstVisibleItemIndex
+        var coercedOffset = minOf(abs(targetScrollOffset), averageSize)
+        if (targetScrollOffset < 0) coercedOffset *= -1
         return (averageSize * indexesDiff).toFloat() +
-            targetScrollOffset - firstVisibleItemScrollOffset
+            coercedOffset - firstVisibleItemScrollOffset
     }
 
     override suspend fun scroll(block: suspend ScrollScope.() -> Unit) {
diff --git a/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/lazy/grid/LazyGridAnimateScrollScope.kt b/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/lazy/grid/LazyGridAnimateScrollScope.kt
index 11963f7..174036d 100644
--- a/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/lazy/grid/LazyGridAnimateScrollScope.kt
+++ b/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/lazy/grid/LazyGridAnimateScrollScope.kt
@@ -20,6 +20,7 @@
 import androidx.compose.foundation.lazy.layout.LazyAnimateScrollScope
 import androidx.compose.ui.unit.Density
 import androidx.compose.ui.util.fastFirstOrNull
+import kotlin.math.abs
 import kotlin.math.max
 
 internal class LazyGridAnimateScrollScope(
@@ -64,8 +65,10 @@
             (index - firstVisibleItemIndex + (slotsPerLine - 1) * if (before) -1 else 1) /
                 slotsPerLine
 
+        var coercedOffset = minOf(abs(targetScrollOffset), averageLineMainAxisSize)
+        if (targetScrollOffset < 0) coercedOffset *= -1
         return (averageLineMainAxisSize * linesDiff).toFloat() +
-            targetScrollOffset - firstVisibleItemScrollOffset
+            coercedOffset - firstVisibleItemScrollOffset
     }
 
     override val numOfItemsForTeleport: Int get() = 100 * state.slotsPerLine
diff --git a/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/lazy/layout/LazyAnimateScroll.kt b/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/lazy/layout/LazyAnimateScroll.kt
index 814a33a..3548bcd 100644
--- a/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/lazy/layout/LazyAnimateScroll.kt
+++ b/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/lazy/layout/LazyAnimateScroll.kt
@@ -33,6 +33,7 @@
 
 private val TargetDistance = 2500.dp
 private val BoundDistance = 1500.dp
+private val MinimumDistance = 50.dp
 
 private const val DEBUG = false
 private inline fun debugLog(generateMsg: () -> String) {
@@ -77,6 +78,7 @@
         try {
             val targetDistancePx = with(density) { TargetDistance.toPx() }
             val boundDistancePx = with(density) { BoundDistance.toPx() }
+            val minDistancePx = with(density) { MinimumDistance.toPx() }
             var loop = true
             var anim = AnimationState(0f)
             val targetItemInitialOffset = getTargetItemOffset(index)
@@ -118,7 +120,8 @@
             while (loop && itemCount > 0) {
                 val expectedDistance = expectedDistanceTo(index, scrollOffset)
                 val target = if (abs(expectedDistance) < targetDistancePx) {
-                    expectedDistance
+                    val absTargetPx = maxOf(abs(expectedDistance), minDistancePx)
+                    if (forward) absTargetPx else -absTargetPx
                 } else {
                     if (forward) targetDistancePx else -targetDistancePx
                 }
diff --git a/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/lazy/layout/LazyPinnableContainerProvider.kt b/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/lazy/layout/LazyPinnableContainerProvider.kt
index a3e794a..5c22dc1 100644
--- a/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/lazy/layout/LazyPinnableContainerProvider.kt
+++ b/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/lazy/layout/LazyPinnableContainerProvider.kt
@@ -69,7 +69,7 @@
 
     /**
      * It is a valid use case when users of this class call [pin] multiple times individually,
-     * so we want to do the unpinning only when all of the users called [unpin].
+     * so we want to do the unpinning only when all of the users called [release].
      */
     private var pinsCount by mutableStateOf(0)
 
@@ -90,7 +90,7 @@
                 if (value !== previous) {
                     _parentPinnableContainer = value
                     if (pinsCount > 0) {
-                        parentHandle?.unpin()
+                        parentHandle?.release()
                         parentHandle = value?.pin()
                     }
                 }
@@ -106,19 +106,19 @@
         return this
     }
 
-    override fun unpin() {
-        check(pinsCount > 0) { "Unpin should only be called once" }
+    override fun release() {
+        check(pinsCount > 0) { "Release should only be called once" }
         pinsCount--
         if (pinsCount == 0) {
             owner.unpin(this)
-            parentHandle?.unpin()
+            parentHandle?.release()
             parentHandle = null
         }
     }
 
     fun onDisposed() {
         repeat(pinsCount) {
-            unpin()
+            release()
         }
     }
 }
diff --git a/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/lazy/staggeredgrid/LazyStaggeredGridAnimateScrollScope.kt b/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/lazy/staggeredgrid/LazyStaggeredGridAnimateScrollScope.kt
index cf958476..57f75d2 100644
--- a/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/lazy/staggeredgrid/LazyStaggeredGridAnimateScrollScope.kt
+++ b/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/lazy/staggeredgrid/LazyStaggeredGridAnimateScrollScope.kt
@@ -21,6 +21,7 @@
 import androidx.compose.foundation.lazy.layout.LazyAnimateScrollScope
 import androidx.compose.ui.unit.Density
 import androidx.compose.ui.util.fastSumBy
+import kotlin.math.abs
 
 @ExperimentalFoundationApi
 internal class LazyStaggeredGridAnimateScrollScope(
@@ -55,8 +56,10 @@
         val averageMainAxisItemSize = itemSizeSum / (visibleItems.size * state.laneCount)
 
         val indexesDiff = index - firstVisibleItemIndex
+        var coercedOffset = minOf(abs(targetScrollOffset), averageMainAxisItemSize)
+        if (targetScrollOffset < 0) coercedOffset *= -1
         return (averageMainAxisItemSize * indexesDiff).toFloat() +
-            targetScrollOffset - firstVisibleItemScrollOffset
+            coercedOffset - firstVisibleItemScrollOffset
     }
 
     override val numOfItemsForTeleport: Int get() = 100 * state.laneCount
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 c9af356..537d000 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
@@ -64,6 +64,7 @@
 import androidx.compose.ui.unit.LayoutDirection
 import androidx.compose.ui.unit.Velocity
 import androidx.compose.ui.unit.dp
+import androidx.compose.ui.util.fastFirstOrNull
 import androidx.compose.ui.util.fastForEach
 import androidx.compose.ui.util.fastSumBy
 import kotlin.math.absoluteValue
@@ -600,12 +601,12 @@
                     SnapAlignmentStartToStart
                 )
 
-                // Find item that is closest to the center
+                // Find item that is closest to the snap position, but before it
                 if (offset <= 0 && offset > lowerBoundOffset) {
                     lowerBoundOffset = offset
                 }
 
-                // Find item that is closest to center, but after it
+                // Find item that is closest to the snap position, but after it
                 if (offset >= 0 && offset < upperBoundOffset) {
                     upperBoundOffset = offset
                 }
@@ -624,10 +625,14 @@
 
         override fun Density.calculateApproachOffset(initialVelocity: Float): Float {
             val effectivePageSizePx = pagerState.pageSize + pagerState.pageSpacing
-            val scrollOffset = pagerState.currentPageOffsetFraction * effectivePageSizePx
             val animationOffsetPx =
                 decayAnimationSpec.calculateTargetValue(0f, initialVelocity)
-            val startPage = pagerState.currentPage
+            val startPage = pagerState.firstVisiblePage?.let {
+                if (initialVelocity < 0) it.index + 1 else it.index
+            } ?: pagerState.currentPage
+
+            val scrollOffset =
+                layoutInfo.visibleItemsInfo.fastFirstOrNull { it.index == startPage }?.offset ?: 0
 
             debugLog {
                 "Initial Offset=$scrollOffset " +
@@ -659,11 +664,13 @@
 
             val proposedFlingOffset = (correctedTargetPage - startPage) * effectivePageSizePx
 
-            val flingApproachOffsetPx =
-                (proposedFlingOffset.absoluteValue - scrollOffset.absoluteValue).coerceAtLeast(0f)
+            debugLog { "Proposed Fling Approach Offset=$proposedFlingOffset" }
 
-            return if (flingApproachOffsetPx == 0f) {
-                flingApproachOffsetPx
+            val flingApproachOffsetPx =
+                (proposedFlingOffset.absoluteValue - scrollOffset.absoluteValue).coerceAtLeast(0)
+
+            return if (flingApproachOffsetPx == 0) {
+                flingApproachOffsetPx.toFloat()
             } else {
                 flingApproachOffsetPx * initialVelocity.sign
             }.also {
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 7680e6a..56cb761 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
@@ -140,6 +140,15 @@
             )
         }
 
+    internal val firstVisiblePage: LazyListItemInfo?
+        get() = visiblePages.lastOrNull {
+            density.calculateDistanceToDesiredSnapPosition(
+                layoutInfo,
+                it,
+                SnapAlignmentStartToStart
+            ) <= 0
+        }
+
     private val distanceToSnapPosition: Float
         get() = closestPageToSnappedPosition?.let {
             density.calculateDistanceToDesiredSnapPosition(
@@ -165,7 +174,7 @@
      * @sample androidx.compose.foundation.samples.ObservingStateChangesInPagerStateSample
      *
      */
-    val currentPage: Int by derivedStateOf { closestPageToSnappedPosition?.index ?: 0 }
+    val currentPage: Int by derivedStateOf { closestPageToSnappedPosition?.index ?: initialPage }
 
     private var animationTargetPage by mutableStateOf(-1)
 
@@ -193,23 +202,23 @@
      * @sample androidx.compose.foundation.samples.ObservingStateChangesInPagerStateSample
      */
     val targetPage: Int by derivedStateOf {
-        if (!isScrollInProgress) {
+        val finalPage = if (!isScrollInProgress) {
             currentPage
         } else if (animationTargetPage != -1) {
             animationTargetPage
+        } else if (snapRemainingScrollOffset == 0.0f) {
+            // act on scroll only
+            if (abs(currentPageOffsetFraction) >= abs(positionThresholdFraction)) {
+                currentPage + currentPageOffsetFraction.sign.toInt()
+            } else {
+                currentPage
+            }
         } else {
-            val offsetFromFling = snapRemainingScrollOffset
-            val scrollDirection = currentPageOffsetFraction.sign
-            val offsetFromScroll =
-                if (abs(currentPageOffsetFraction) >= abs(positionThresholdFraction)) {
-                    (abs(currentPageOffsetFraction) + 1) * pageAvailableSpace * scrollDirection
-                } else {
-                    0f
-                }
-            val pageDisplacement =
-                (offsetFromFling + offsetFromScroll) / pageAvailableSpace
-            (currentPage + pageDisplacement.roundToInt()).coerceInPageRange()
+            // act on flinging
+            val pageDisplacement = snapRemainingScrollOffset / pageAvailableSpace
+            (currentPage + pageDisplacement.roundToInt())
         }
+        finalPage.coerceInPageRange()
     }
 
     /**
@@ -250,6 +259,7 @@
      * destination page will be offset from its snapped position.
      */
     suspend fun scrollToPage(page: Int, pageOffsetFraction: Float = 0f) {
+        debugLog { "Scroll from page=$currentPage to page=$page" }
         awaitScrollDependencies()
         require(pageOffsetFraction in -0.5..0.5) {
             "pageOffsetFraction $pageOffsetFraction is not within the range -0.5 to 0.5"
@@ -315,9 +325,7 @@
 
         val displacement = targetOffset - currentOffset + pageOffsetToSnappedPosition
 
-        debugLog {
-            "animateScrollToPage $displacement pixels"
-        }
+        debugLog { "animateScrollToPage $displacement pixels" }
         requireNotNull(lazyListState).animateScrollBy(displacement, animationSpec)
         animationTargetPage = -1
     }
@@ -397,7 +405,7 @@
 private const val MaxPageOffset = 0.5f
 internal val SnapAlignmentStartToStart: Density.(layoutSize: Float, itemSize: Float) -> Float =
     { _, _ -> 0f }
-private val DefaultPositionThreshold = 56.dp
+internal val DefaultPositionThreshold = 56.dp
 private const val MaxPagesForAnimateScroll = 3
 
 private class AwaitLazyListStateSet {
diff --git a/compose/integration-tests/demos/src/main/java/androidx/compose/integration/demos/DemoActivity.kt b/compose/integration-tests/demos/src/main/java/androidx/compose/integration/demos/DemoActivity.kt
index 42befbe..d891805 100644
--- a/compose/integration-tests/demos/src/main/java/androidx/compose/integration/demos/DemoActivity.kt
+++ b/compose/integration-tests/demos/src/main/java/androidx/compose/integration/demos/DemoActivity.kt
@@ -139,7 +139,7 @@
         const val DEMO_NAME = "demoname"
 
         internal fun requireDemo(demoName: String, demo: Demo?) = requireNotNull(demo) {
-            "No demo called \"$demoName\" could be found."
+            "No demo called \"$demoName\" could be found. Note substring matches are allowed."
         }
     }
 }
@@ -239,20 +239,26 @@
             restore = { restored ->
                 require(restored.isNotEmpty())
                 val backStack = restored.mapTo(mutableListOf()) {
-                    requireNotNull(findDemo(rootDemo, it))
+                    requireNotNull(findDemo(rootDemo, it, exact = true))
                 }
                 val initial = backStack.removeAt(backStack.lastIndex)
                 Navigator(backDispatcher, launchActivityDemo, rootDemo, initial, backStack)
             }
         )
 
-        fun findDemo(demo: Demo, title: String): Demo? {
-            if (demo.title == title) {
-                return demo
+        fun findDemo(demo: Demo, title: String, exact: Boolean = false): Demo? {
+            if (exact) {
+                if (demo.title == title) {
+                    return demo
+                }
+            } else {
+                if (demo.title.contains(title)) {
+                    return demo
+                }
             }
             if (demo is DemoCategory) {
                 demo.demos.forEach { child ->
-                    findDemo(child, title)
+                    findDemo(child, title, exact)
                         ?.let { return it }
                 }
             }
diff --git a/compose/lint/common-test/src/main/java/androidx/compose/lint/test/Stubs.kt b/compose/lint/common-test/src/main/java/androidx/compose/lint/test/Stubs.kt
index ed83abb..dd46067 100644
--- a/compose/lint/common-test/src/main/java/androidx/compose/lint/test/Stubs.kt
+++ b/compose/lint/common-test/src/main/java/androidx/compose/lint/test/Stubs.kt
@@ -18,9 +18,9 @@
 
 package androidx.compose.lint.test
 
-import com.android.tools.lint.checks.infrastructure.LintDetectorTest.compiled
 import com.android.tools.lint.checks.infrastructure.LintDetectorTest.kotlin
 import com.android.tools.lint.checks.infrastructure.TestFile
+import com.android.tools.lint.checks.infrastructure.TestFiles.bytecode
 import org.intellij.lang.annotations.Language
 import java.util.Locale
 
@@ -28,7 +28,7 @@
  * Common Compose-related bytecode lint stubs used for testing
  */
 object Stubs {
-    val Color: TestFile = compiledStub(
+    val Color: TestFile = bytecodeStub(
         filename = "Color.kt",
         filepath = "androidx/compose/ui/graphics",
         checksum = 0x2a148ced,
@@ -179,7 +179,7 @@
         """
     )
 
-    val Composable: TestFile = compiledStub(
+    val Composable: TestFile = bytecodeStub(
         filename = "Composable.kt",
         filepath = "androidx/compose/runtime",
         checksum = 0x12c49724,
@@ -219,7 +219,7 @@
         """
     )
 
-    val Modifier: TestFile = compiledStub(
+    val Modifier: TestFile = bytecodeStub(
         filename = "Modifier.kt",
         filepath = "androidx/compose/ui",
         checksum = 0xe49bcfc1,
@@ -348,7 +348,7 @@
         """
     )
 
-    val PaddingValues: TestFile = compiledStub(
+    val PaddingValues: TestFile = bytecodeStub(
         filename = "Padding.kt",
         filepath = "androidx/compose/foundation/layout",
         checksum = 0xeedd3f96,
@@ -379,7 +379,7 @@
         """
     )
 
-    val Remember: TestFile = compiledStub(
+    val Remember: TestFile = bytecodeStub(
         filename = "Remember.kt",
         filepath = "androidx/compose/runtime",
         checksum = 0xc78323f1,
@@ -454,7 +454,7 @@
         """
     )
 
-    val SnapshotState: TestFile = compiledStub(
+    val SnapshotState: TestFile = bytecodeStub(
         filename = "SnapshotState.kt",
         filepath = "androidx/compose/runtime",
         checksum = 0x9907976f,
@@ -694,7 +694,7 @@
                 """
     )
 
-    val Dp: TestFile = compiledStub(
+    val Dp: TestFile = bytecodeStub(
         filename = "Dp.kt",
         filepath = "androidx/compose/ui/unit",
         checksum = 0x9e27930c,
@@ -764,7 +764,7 @@
                 """
     )
 
-    val Animatable: TestFile = compiledStub(
+    val Animatable: TestFile = bytecodeStub(
         filename = "Animatable.kt",
         filepath = "androidx/compose/animation/core",
         checksum = 0x68ff47da,
@@ -831,7 +831,7 @@
                 """
     )
 
-    val IntOffset: TestFile = compiledStub(
+    val IntOffset: TestFile = bytecodeStub(
         filename = "IntOffset.kt",
         filepath = "androidx/compose/ui/unit",
         checksum = 0xfd7af994,
@@ -864,7 +864,7 @@
 }
 
 /**
- * Utility for creating a [kotlin] and corresponding [compiled] stub, to try and make it easier to
+ * Utility for creating a [kotlin] and corresponding [bytecode] stub, to try and make it easier to
  * configure everything correctly.
  *
  * @param filename name of the Kotlin source file, with extension - e.g. "Test.kt". These should
@@ -877,33 +877,33 @@
  * @param bytecode generated bytecode that will be used in tests. Leave empty to generate the
  * bytecode for [source].
  *
- * @return a pair of kotlin test file, to compiled test file
+ * @return a pair of kotlin test file, to bytecode test file
  */
-fun kotlinAndCompiledStub(
+fun kotlinAndBytecodeStub(
     filename: String,
     filepath: String,
     checksum: Long,
     @Language("kotlin") source: String,
     vararg bytecode: String
-): KotlinAndCompiledStub {
+): KotlinAndBytecodeStub {
     val filenameWithoutExtension = filename.substringBefore(".").lowercase(Locale.ROOT)
     val kotlin = kotlin(source).to("$filepath/$filename")
-    val compiled = compiled(
+    val bytecodeStub = bytecode(
         "libs/$filenameWithoutExtension.jar",
         kotlin,
         checksum,
         *bytecode
     )
-    return KotlinAndCompiledStub(kotlin, compiled)
+    return KotlinAndBytecodeStub(kotlin, bytecodeStub)
 }
 
-class KotlinAndCompiledStub(
+class KotlinAndBytecodeStub(
     val kotlin: TestFile,
-    val compiled: TestFile
+    val bytecode: TestFile
 )
 
 /**
- * Utility for creating a [compiled] stub, to try and make it easier to configure everything
+ * Utility for creating a [bytecode] stub, to try and make it easier to configure everything
  * correctly.
  *
  * @param filename name of the Kotlin source file, with extension - e.g. "Test.kt". These should
@@ -916,10 +916,10 @@
  * @param bytecode generated bytecode that will be used in tests. Leave empty to generate the
  * bytecode for [source].
  */
-fun compiledStub(
+fun bytecodeStub(
     filename: String,
     filepath: String,
     checksum: Long,
     @Language("kotlin") source: String,
     vararg bytecode: String
-): TestFile = kotlinAndCompiledStub(filename, filepath, checksum, source, *bytecode).compiled
+): TestFile = kotlinAndBytecodeStub(filename, filepath, checksum, source, *bytecode).bytecode
diff --git a/compose/lint/internal-lint-checks/src/test/java/androidx/compose/lint/ModifierInspectorInfoDetectorTest.kt b/compose/lint/internal-lint-checks/src/test/java/androidx/compose/lint/ModifierInspectorInfoDetectorTest.kt
index ab8faf7..25fa03f 100644
--- a/compose/lint/internal-lint-checks/src/test/java/androidx/compose/lint/ModifierInspectorInfoDetectorTest.kt
+++ b/compose/lint/internal-lint-checks/src/test/java/androidx/compose/lint/ModifierInspectorInfoDetectorTest.kt
@@ -608,6 +608,7 @@
     @Test
     fun rememberModifierInfo() {
         lint().files(
+            Stubs.Composable,
             Stubs.Modifier,
             composedStub,
             Stubs.Remember,
@@ -647,6 +648,7 @@
     @Test
     fun emptyModifier() {
         lint().files(
+            Stubs.Composable,
             Stubs.Modifier,
             Stubs.Remember,
             composedStub,
diff --git a/compose/lint/internal-lint-checks/src/test/java/androidx/compose/lint/UnnecessaryLambdaCreationDetectorTest.kt b/compose/lint/internal-lint-checks/src/test/java/androidx/compose/lint/UnnecessaryLambdaCreationDetectorTest.kt
index c96667d..e8aa463 100644
--- a/compose/lint/internal-lint-checks/src/test/java/androidx/compose/lint/UnnecessaryLambdaCreationDetectorTest.kt
+++ b/compose/lint/internal-lint-checks/src/test/java/androidx/compose/lint/UnnecessaryLambdaCreationDetectorTest.kt
@@ -19,7 +19,7 @@
 package androidx.compose.lint
 
 import androidx.compose.lint.test.Stubs
-import androidx.compose.lint.test.kotlinAndCompiledStub
+import androidx.compose.lint.test.kotlinAndBytecodeStub
 import com.android.tools.lint.checks.infrastructure.TestLintResult
 import org.junit.Test
 import org.junit.runner.RunWith
@@ -39,7 +39,7 @@
     private val stub: TestFile
 ) : LintDetectorTest() {
     companion object {
-        private val stub = kotlinAndCompiledStub(
+        private val stub = kotlinAndBytecodeStub(
             filename = "Stub.kt",
             filepath = "test",
             checksum = 0xdbff73f0,
@@ -97,7 +97,7 @@
         @Parameterized.Parameters(name = "{0}")
         fun params(): Array<Any> = arrayOf(
             arrayOf("Source stubs", stub.kotlin),
-            arrayOf("Compiled stubs", stub.compiled)
+            arrayOf("Compiled stubs", stub.bytecode)
         )
     }
 
diff --git a/compose/material/material-lint/src/test/java/androidx/compose/material/lint/ColorsDetectorTest.kt b/compose/material/material-lint/src/test/java/androidx/compose/material/lint/ColorsDetectorTest.kt
index 832a570..34438f0 100644
--- a/compose/material/material-lint/src/test/java/androidx/compose/material/lint/ColorsDetectorTest.kt
+++ b/compose/material/material-lint/src/test/java/androidx/compose/material/lint/ColorsDetectorTest.kt
@@ -19,7 +19,7 @@
 package androidx.compose.material.lint
 
 import androidx.compose.lint.test.Stubs
-import androidx.compose.lint.test.kotlinAndCompiledStub
+import androidx.compose.lint.test.kotlinAndBytecodeStub
 import com.android.tools.lint.checks.infrastructure.LintDetectorTest
 import com.android.tools.lint.detector.api.Detector
 import com.android.tools.lint.detector.api.Issue
@@ -44,7 +44,7 @@
     override fun getIssues(): MutableList<Issue> = mutableListOf(ColorsDetector.ConflictingOnColor)
 
     // Simplified Colors.kt stubs
-    private val ColorsStub = kotlinAndCompiledStub(
+    private val ColorsStub = kotlinAndBytecodeStub(
         filename = "Colors.kt",
         filepath = "androidx/compose/material",
         checksum = 0x2f84988c,
@@ -583,7 +583,7 @@
             """
             ),
             Stubs.Color,
-            ColorsStub.compiled
+            ColorsStub.bytecode
         )
             .run()
             // TODO: b/184856104 currently the constructor call to Colors cannot be resolved when
@@ -614,7 +614,7 @@
             """
             ),
             Stubs.Color,
-            ColorsStub.compiled
+            ColorsStub.bytecode
         )
             .run()
             .expect(
@@ -653,7 +653,7 @@
             """
             ),
             Stubs.Color,
-            ColorsStub.compiled
+            ColorsStub.bytecode
         )
             .run()
             .expect(
@@ -711,7 +711,7 @@
             """
             ),
             Stubs.Color,
-            ColorsStub.compiled
+            ColorsStub.bytecode
         )
             .run()
             .expectClean()
@@ -784,7 +784,7 @@
             """
             ),
             Stubs.Color,
-            ColorsStub.compiled
+            ColorsStub.bytecode
         )
             .run()
             .expectClean()
diff --git a/compose/material/material-lint/src/test/java/androidx/compose/material/lint/ScaffoldPaddingDetectorTest.kt b/compose/material/material-lint/src/test/java/androidx/compose/material/lint/ScaffoldPaddingDetectorTest.kt
index 7537ea8..65d5116 100644
--- a/compose/material/material-lint/src/test/java/androidx/compose/material/lint/ScaffoldPaddingDetectorTest.kt
+++ b/compose/material/material-lint/src/test/java/androidx/compose/material/lint/ScaffoldPaddingDetectorTest.kt
@@ -19,7 +19,7 @@
 package androidx.compose.material.lint
 
 import androidx.compose.lint.test.Stubs
-import androidx.compose.lint.test.compiledStub
+import androidx.compose.lint.test.bytecodeStub
 import com.android.tools.lint.checks.infrastructure.LintDetectorTest
 import com.android.tools.lint.detector.api.Detector
 import com.android.tools.lint.detector.api.Issue
@@ -40,7 +40,7 @@
         mutableListOf(ScaffoldPaddingDetector.UnusedMaterialScaffoldPaddingParameter)
 
     // Simplified Scaffold.kt stubs
-    private val ScaffoldStub = compiledStub(
+    private val ScaffoldStub = bytecodeStub(
         filename = "Scaffold.kt",
         filepath = "androidx/compose/material",
         checksum = 0x2dde3750,
diff --git a/compose/material/material/api/public_plus_experimental_current.txt b/compose/material/material/api/public_plus_experimental_current.txt
index a062b45..e54f249 100644
--- a/compose/material/material/api/public_plus_experimental_current.txt
+++ b/compose/material/material/api/public_plus_experimental_current.txt
@@ -483,8 +483,10 @@
 
   public final class InteractiveComponentSizeKt {
     method @androidx.compose.material.ExperimentalMaterialApi public static androidx.compose.runtime.ProvidableCompositionLocal<java.lang.Boolean> getLocalMinimumInteractiveComponentEnforcement();
+    method @Deprecated @androidx.compose.material.ExperimentalMaterialApi public static androidx.compose.runtime.ProvidableCompositionLocal<java.lang.Boolean> getLocalMinimumTouchTargetEnforcement();
     method public static androidx.compose.ui.Modifier minimumInteractiveComponentSize(androidx.compose.ui.Modifier);
     property @androidx.compose.material.ExperimentalMaterialApi public static final androidx.compose.runtime.ProvidableCompositionLocal<java.lang.Boolean> LocalMinimumInteractiveComponentEnforcement;
+    property @Deprecated @androidx.compose.material.ExperimentalMaterialApi public static final androidx.compose.runtime.ProvidableCompositionLocal<java.lang.Boolean> LocalMinimumTouchTargetEnforcement;
   }
 
   public final class ListItemKt {
diff --git a/compose/material/material/build.gradle b/compose/material/material/build.gradle
index ebd1cd5..53e8375 100644
--- a/compose/material/material/build.gradle
+++ b/compose/material/material/build.gradle
@@ -48,7 +48,7 @@
         // TODO: remove next 3 dependencies when b/202810604 is fixed
         implementation("androidx.savedstate:savedstate:1.1.0")
         implementation("androidx.lifecycle:lifecycle-runtime:2.5.1")
-        implementation("androidx.lifecycle:lifecycle-viewmodel:2.5.1")
+        implementation(project(":lifecycle:lifecycle-viewmodel"))
 
         testImplementation(libs.testRules)
         testImplementation(libs.testRunner)
diff --git a/compose/material/material/src/androidMain/kotlin/androidx/compose/material/internal/ExposedDropdownMenuPopup.kt b/compose/material/material/src/androidMain/kotlin/androidx/compose/material/internal/ExposedDropdownMenuPopup.kt
index 4e6838a..006240b 100644
--- a/compose/material/material/src/androidMain/kotlin/androidx/compose/material/internal/ExposedDropdownMenuPopup.kt
+++ b/compose/material/material/src/androidMain/kotlin/androidx/compose/material/internal/ExposedDropdownMenuPopup.kt
@@ -66,7 +66,8 @@
 import androidx.compose.ui.window.PopupPositionProvider
 import androidx.lifecycle.findViewTreeLifecycleOwner
 import androidx.lifecycle.setViewTreeLifecycleOwner
-import androidx.lifecycle.ViewTreeViewModelStoreOwner
+import androidx.lifecycle.findViewTreeViewModelStoreOwner
+import androidx.lifecycle.setViewTreeViewModelStoreOwner
 import androidx.savedstate.findViewTreeSavedStateRegistryOwner
 import androidx.savedstate.setViewTreeSavedStateRegistryOwner
 import java.util.UUID
@@ -257,7 +258,7 @@
     init {
         id = android.R.id.content
         setViewTreeLifecycleOwner(composeView.findViewTreeLifecycleOwner())
-        ViewTreeViewModelStoreOwner.set(this, ViewTreeViewModelStoreOwner.get(composeView))
+        setViewTreeViewModelStoreOwner(composeView.findViewTreeViewModelStoreOwner())
         setViewTreeSavedStateRegistryOwner(composeView.findViewTreeSavedStateRegistryOwner())
         composeView.viewTreeObserver.addOnGlobalLayoutListener(this)
         // Set unique id for AbstractComposeView. This allows state restoration for the state
diff --git a/compose/material/material/src/commonMain/kotlin/androidx/compose/material/InteractiveComponentSize.kt b/compose/material/material/src/commonMain/kotlin/androidx/compose/material/InteractiveComponentSize.kt
index ac5121d..220666b 100644
--- a/compose/material/material/src/commonMain/kotlin/androidx/compose/material/InteractiveComponentSize.kt
+++ b/compose/material/material/src/commonMain/kotlin/androidx/compose/material/InteractiveComponentSize.kt
@@ -75,6 +75,27 @@
 val LocalMinimumInteractiveComponentEnforcement: ProvidableCompositionLocal<Boolean> =
     staticCompositionLocalOf { true }
 
+/**
+ * CompositionLocal that configures whether Material components that have a visual size that is
+ * lower than the minimum touch target size for accessibility (such as [Button]) will include
+ * extra space outside the component to ensure that they are accessible. If set to false there
+ * will be no extra space, and so it is possible that if the component is placed near the edge of
+ * a layout / near to another component without any padding, there will not be enough space for
+ * an accessible touch target.
+ */
+@Suppress("OPT_IN_MARKER_ON_WRONG_TARGET")
+@get:ExperimentalMaterialApi
+@ExperimentalMaterialApi
+@Deprecated(
+    message = "Use LocalMinimumInteractiveComponentEnforcement instead.",
+    replaceWith = ReplaceWith(
+        "LocalMinimumInteractiveComponentEnforcement"
+    ),
+    level = DeprecationLevel.WARNING
+)
+val LocalMinimumTouchTargetEnforcement: ProvidableCompositionLocal<Boolean> =
+    LocalMinimumInteractiveComponentEnforcement
+
 private class MinimumInteractiveComponentSizeModifier(val size: DpSize) : LayoutModifier {
     override fun MeasureScope.measure(
         measurable: Measurable,
diff --git a/compose/material3/material3-lint/src/test/java/androidx/compose/material3/lint/ScaffoldPaddingDetectorTest.kt b/compose/material3/material3-lint/src/test/java/androidx/compose/material3/lint/ScaffoldPaddingDetectorTest.kt
index 1fb1874..014f0ab 100644
--- a/compose/material3/material3-lint/src/test/java/androidx/compose/material3/lint/ScaffoldPaddingDetectorTest.kt
+++ b/compose/material3/material3-lint/src/test/java/androidx/compose/material3/lint/ScaffoldPaddingDetectorTest.kt
@@ -19,7 +19,7 @@
 package androidx.compose.material3.lint
 
 import androidx.compose.lint.test.Stubs
-import androidx.compose.lint.test.compiledStub
+import androidx.compose.lint.test.bytecodeStub
 import com.android.tools.lint.checks.infrastructure.LintDetectorTest
 import com.android.tools.lint.detector.api.Detector
 import com.android.tools.lint.detector.api.Issue
@@ -40,7 +40,7 @@
         mutableListOf(ScaffoldPaddingDetector.UnusedMaterial3ScaffoldPaddingParameter)
 
     // Simplified Scaffold.kt stubs
-    private val ScaffoldStub = compiledStub(
+    private val ScaffoldStub = bytecodeStub(
         filename = "Scaffold.kt",
         filepath = "androidx/compose/material3",
         checksum = 0xfee46355,
diff --git a/compose/material3/material3/api/public_plus_experimental_current.txt b/compose/material3/material3/api/public_plus_experimental_current.txt
index c06bac2..45313fd 100644
--- a/compose/material3/material3/api/public_plus_experimental_current.txt
+++ b/compose/material3/material3/api/public_plus_experimental_current.txt
@@ -577,8 +577,10 @@
 
   public final class InteractiveComponentSizeKt {
     method @androidx.compose.material3.ExperimentalMaterial3Api public static androidx.compose.runtime.ProvidableCompositionLocal<java.lang.Boolean> getLocalMinimumInteractiveComponentEnforcement();
+    method @Deprecated @androidx.compose.material3.ExperimentalMaterial3Api public static androidx.compose.runtime.ProvidableCompositionLocal<java.lang.Boolean> getLocalMinimumTouchTargetEnforcement();
     method public static androidx.compose.ui.Modifier minimumInteractiveComponentSize(androidx.compose.ui.Modifier);
     property @androidx.compose.material3.ExperimentalMaterial3Api public static final androidx.compose.runtime.ProvidableCompositionLocal<java.lang.Boolean> LocalMinimumInteractiveComponentEnforcement;
+    property @Deprecated @androidx.compose.material3.ExperimentalMaterial3Api public static final androidx.compose.runtime.ProvidableCompositionLocal<java.lang.Boolean> LocalMinimumTouchTargetEnforcement;
   }
 
   @androidx.compose.material3.ExperimentalMaterial3Api @androidx.compose.runtime.Immutable public final class ListItemColors {
diff --git a/compose/material3/material3/build.gradle b/compose/material3/material3/build.gradle
index 4b3e912..279836b 100644
--- a/compose/material3/material3/build.gradle
+++ b/compose/material3/material3/build.gradle
@@ -51,7 +51,7 @@
         // TODO: remove next 3 dependencies when b/202810604 is fixed
         implementation("androidx.savedstate:savedstate-ktx:1.2.0")
         implementation("androidx.lifecycle:lifecycle-runtime:2.5.1")
-        implementation("androidx.lifecycle:lifecycle-viewmodel:2.5.1")
+        implementation(project(":lifecycle:lifecycle-viewmodel"))
 
         testImplementation(libs.testRules)
         testImplementation(libs.testRunner)
diff --git a/compose/material3/material3/src/androidAndroidTest/kotlin/androidx/compose/material3/DatePickerTest.kt b/compose/material3/material3/src/androidAndroidTest/kotlin/androidx/compose/material3/DatePickerTest.kt
index c17e736..a22a0342 100644
--- a/compose/material3/material3/src/androidAndroidTest/kotlin/androidx/compose/material3/DatePickerTest.kt
+++ b/compose/material3/material3/src/androidAndroidTest/kotlin/androidx/compose/material3/DatePickerTest.kt
@@ -16,10 +16,15 @@
 
 package androidx.compose.material3
 
+import androidx.compose.ui.semantics.Role
+import androidx.compose.ui.semantics.SemanticsProperties
+import androidx.compose.ui.test.SemanticsMatcher.Companion.expectValue
+import androidx.compose.ui.test.assert
+import androidx.compose.ui.test.assertContentDescriptionEquals
 import androidx.compose.ui.test.assertIsEnabled
 import androidx.compose.ui.test.assertIsNotEnabled
-import androidx.compose.ui.test.assertIsOff
-import androidx.compose.ui.test.assertIsOn
+import androidx.compose.ui.test.assertIsNotSelected
+import androidx.compose.ui.test.assertIsSelected
 import androidx.compose.ui.test.junit4.StateRestorationTester
 import androidx.compose.ui.test.junit4.createComposeRule
 import androidx.compose.ui.test.onAllNodesWithText
@@ -31,6 +36,7 @@
 import androidx.test.filters.MediumTest
 import com.google.common.truth.Truth.assertThat
 import java.util.Calendar
+import java.util.Locale
 import java.util.TimeZone
 import org.junit.Rule
 import org.junit.Test
@@ -58,7 +64,7 @@
         }
 
         // Select the 11th day of the displayed month is selected.
-        rule.onNodeWithText("11").assertIsOn()
+        rule.onNodeWithText("11").assertIsSelected()
         rule.onNodeWithText("May 11, 2010").assertExists()
     }
 
@@ -78,7 +84,7 @@
         rule.onNodeWithText(defaultHeadline).assertExists()
 
         // Select the 27th day of the displayed month.
-        rule.onNodeWithText("27").assertIsOff()
+        rule.onNodeWithText("27").assertIsNotSelected()
         rule.onNodeWithText("27").performClick()
 
         rule.runOnIdle {
@@ -93,7 +99,7 @@
 
         rule.onNodeWithText(defaultHeadline).assertDoesNotExist()
         rule.onNodeWithText("Jan 27, 2019").assertExists()
-        rule.onNodeWithText("27").assertIsOn()
+        rule.onNodeWithText("27").assertIsSelected()
     }
 
     @Test
@@ -136,7 +142,7 @@
         }
 
         rule.onNodeWithText("January 2019").performClick()
-        rule.onNodeWithText("2019").assertIsOn()
+        rule.onNodeWithText("2019").assertIsSelected()
         rule.onNodeWithText("2020").performClick()
         // Select the 15th day of the displayed month in 2020.
         rule.onAllNodesWithText("15").onFirst().performClick()
@@ -153,8 +159,8 @@
 
         // Check that if the years are opened again, the last selected year is still marked as such
         rule.onNodeWithText("January 2020").performClick()
-        rule.onNodeWithText("2019").assertIsOff()
-        rule.onNodeWithText("2020").assertIsOn()
+        rule.onNodeWithText("2019").assertIsNotSelected()
+        rule.onNodeWithText("2020").assertIsSelected()
     }
 
     @Test
@@ -366,6 +372,43 @@
         }
     }
 
+    @Test
+    fun defaultSemantics() {
+        val selectedDateInUtcMillis = dayInUtcMilliseconds(year = 2010, month = 5, dayOfMonth = 11)
+        val monthInUtcMillis = dayInUtcMilliseconds(year = 2010, month = 5, dayOfMonth = 1)
+        lateinit var expectedHeadlineStringFormat: String
+        rule.setMaterialContent(lightColorScheme()) {
+            // e.g. "Current selection: %1$s"
+            expectedHeadlineStringFormat = getString(Strings.DatePickerHeadlineDescription)
+            DatePicker(
+                datePickerState = rememberDatePickerState(
+                    initialSelectedDateMillis = selectedDateInUtcMillis,
+                    initialDisplayedMonthMillis = monthInUtcMillis
+                )
+            )
+        }
+
+        val fullDateDescription = formatWithSkeleton(
+            selectedDateInUtcMillis,
+            DatePickerDefaults.YearMonthWeekdayDaySkeleton,
+            Locale.US
+        )
+
+        rule.onNodeWithContentDescription(label = "next", substring = true, ignoreCase = true)
+            .assert(expectValue(SemanticsProperties.Role, Role.Button))
+        rule.onNodeWithContentDescription(label = "previous", substring = true, ignoreCase = true)
+            .assert(expectValue(SemanticsProperties.Role, Role.Button))
+        rule.onNodeWithText("May 2010")
+            .assert(expectValue(SemanticsProperties.Role, Role.Button))
+        rule.onNodeWithText("11")
+            .assert(expectValue(SemanticsProperties.Role, Role.Button))
+            .assertContentDescriptionEquals(fullDateDescription)
+        rule.onNodeWithText("May 11, 2010")
+            .assertContentDescriptionEquals(
+                expectedHeadlineStringFormat.format(fullDateDescription)
+            )
+    }
+
     // Returns the given date's day as milliseconds from epoch. The returned value is for the day's
     // start on midnight.
     private fun dayInUtcMilliseconds(year: Int, month: Int, dayOfMonth: Int): Long {
diff --git a/compose/material3/material3/src/androidAndroidTest/kotlin/androidx/compose/material3/TooltipTest.kt b/compose/material3/material3/src/androidAndroidTest/kotlin/androidx/compose/material3/TooltipTest.kt
index cc8b1ea..ae58d4b 100644
--- a/compose/material3/material3/src/androidAndroidTest/kotlin/androidx/compose/material3/TooltipTest.kt
+++ b/compose/material3/material3/src/androidAndroidTest/kotlin/androidx/compose/material3/TooltipTest.kt
@@ -35,9 +35,9 @@
 import androidx.compose.ui.test.performTouchInput
 import androidx.compose.ui.unit.dp
 import androidx.test.ext.junit.runners.AndroidJUnit4
-import androidx.test.filters.FlakyTest
 import androidx.test.filters.MediumTest
 import kotlinx.coroutines.launch
+import org.junit.Ignore
 import org.junit.Rule
 import org.junit.Test
 import org.junit.runner.RunWith
@@ -75,7 +75,7 @@
             .assertWidthIsEqualTo(customWidth)
     }
 
-    @FlakyTest(bugId = 264907895)
+    @Ignore // b/264907895
     @Test
     fun plainTooltip_content_padding() {
         rule.setMaterialContent(lightColorScheme()) {
@@ -94,7 +94,7 @@
             .assertTopPositionInRootIsEqualTo(4.dp)
     }
 
-    @FlakyTest(bugId = 264887805)
+    @Ignore // b/264887805
     @Test
     fun plainTooltip_behavior() {
         rule.setMaterialContent(lightColorScheme()) {
diff --git a/compose/material3/material3/src/androidMain/kotlin/androidx/compose/material3/Strings.android.kt b/compose/material3/material3/src/androidMain/kotlin/androidx/compose/material3/Strings.android.kt
index f3ca92b..9059aad 100644
--- a/compose/material3/material3/src/androidMain/kotlin/androidx/compose/material3/Strings.android.kt
+++ b/compose/material3/material3/src/androidMain/kotlin/androidx/compose/material3/Strings.android.kt
@@ -50,6 +50,9 @@
         Strings.DatePickerHeadline -> resources.getString(
             androidx.compose.material3.R.string.date_picker_headline
         )
+        Strings.DatePickerYearPickerPaneTitle -> resources.getString(
+            androidx.compose.material3.R.string.date_picker_year_picker_pane_title
+        )
         Strings.DatePickerSwitchToYearSelection -> resources.getString(
             androidx.compose.material3.R.string.date_picker_switch_to_year_selection
         )
@@ -62,6 +65,15 @@
         Strings.DatePickerSwitchToPreviousMonth -> resources.getString(
             androidx.compose.material3.R.string.date_picker_switch_to_previous_month
         )
+        Strings.DatePickerNavigateToYearDescription -> resources.getString(
+            androidx.compose.material3.R.string.date_picker_navigate_to_year_description
+        )
+        Strings.DatePickerHeadlineDescription -> resources.getString(
+            androidx.compose.material3.R.string.date_picker_headline_description
+        )
+        Strings.DatePickerNoSelectionDescription -> resources.getString(
+            androidx.compose.material3.R.string.date_picker_no_selection_description
+        )
         else -> ""
     }
 }
diff --git a/compose/material3/material3/src/androidMain/kotlin/androidx/compose/material3/internal/ExposedDropdownMenuPopup.kt b/compose/material3/material3/src/androidMain/kotlin/androidx/compose/material3/internal/ExposedDropdownMenuPopup.kt
index 6eeb94b..d18f90d 100644
--- a/compose/material3/material3/src/androidMain/kotlin/androidx/compose/material3/internal/ExposedDropdownMenuPopup.kt
+++ b/compose/material3/material3/src/androidMain/kotlin/androidx/compose/material3/internal/ExposedDropdownMenuPopup.kt
@@ -66,7 +66,8 @@
 import androidx.compose.ui.window.PopupPositionProvider
 import androidx.lifecycle.setViewTreeLifecycleOwner
 import androidx.lifecycle.findViewTreeLifecycleOwner
-import androidx.lifecycle.ViewTreeViewModelStoreOwner
+import androidx.lifecycle.findViewTreeViewModelStoreOwner
+import androidx.lifecycle.setViewTreeViewModelStoreOwner
 import androidx.savedstate.findViewTreeSavedStateRegistryOwner
 import androidx.savedstate.setViewTreeSavedStateRegistryOwner
 import java.util.UUID
@@ -258,7 +259,7 @@
     init {
         id = android.R.id.content
         setViewTreeLifecycleOwner(composeView.findViewTreeLifecycleOwner())
-        ViewTreeViewModelStoreOwner.set(this, ViewTreeViewModelStoreOwner.get(composeView))
+        setViewTreeViewModelStoreOwner(composeView.findViewTreeViewModelStoreOwner())
         setViewTreeSavedStateRegistryOwner(composeView.findViewTreeSavedStateRegistryOwner())
         composeView.viewTreeObserver.addOnGlobalLayoutListener(this)
         // Set unique id for AbstractComposeView. This allows state restoration for the state
diff --git a/compose/material3/material3/src/androidMain/res/values/strings.xml b/compose/material3/material3/src/androidMain/res/values/strings.xml
index 9284a12..6949cde 100644
--- a/compose/material3/material3/src/androidMain/res/values/strings.xml
+++ b/compose/material3/material3/src/androidMain/res/values/strings.xml
@@ -32,7 +32,13 @@
     <string name="date_picker_title">"Select date"</string>
     <string name="date_picker_headline">"Selected date"</string>
     <string name="date_picker_switch_to_year_selection">"Switch to selecting a year"</string>
-    <string name="date_picker_switch_to_day_selection">"Switch to selecting a day"</string>
+    <string name="date_picker_switch_to_day_selection">
+        "Swipe to select a year, or tap to switch back to selecting a day"
+    </string>
     <string name="date_picker_switch_to_next_month">"Change to next month"</string>
     <string name="date_picker_switch_to_previous_month">"Change to previous month"</string>
+    <string name="date_picker_navigate_to_year_description">Navigate to year %1$s</string>
+    <string name="date_picker_headline_description">Current selection: %1$s</string>
+    <string name="date_picker_no_selection_description">None</string>
+    <string name="date_picker_year_picker_pane_title">Year picker visible</string>
 </resources>
diff --git a/compose/material3/material3/src/commonMain/kotlin/androidx/compose/material3/DatePicker.kt b/compose/material3/material3/src/commonMain/kotlin/androidx/compose/material3/DatePicker.kt
index 602bf70..850dbfc 100644
--- a/compose/material3/material3/src/commonMain/kotlin/androidx/compose/material3/DatePicker.kt
+++ b/compose/material3/material3/src/commonMain/kotlin/androidx/compose/material3/DatePicker.kt
@@ -82,8 +82,17 @@
 import androidx.compose.ui.graphics.Shape
 import androidx.compose.ui.platform.LocalDensity
 import androidx.compose.ui.platform.LocalLayoutDirection
+import androidx.compose.ui.semantics.LiveRegionMode
+import androidx.compose.ui.semantics.Role
+import androidx.compose.ui.semantics.ScrollAxisRange
 import androidx.compose.ui.semantics.clearAndSetSemantics
 import androidx.compose.ui.semantics.contentDescription
+import androidx.compose.ui.semantics.horizontalScrollAxisRange
+import androidx.compose.ui.semantics.liveRegion
+import androidx.compose.ui.semantics.paneTitle
+import androidx.compose.ui.semantics.role
+import androidx.compose.ui.semantics.semantics
+import androidx.compose.ui.semantics.verticalScrollAxisRange
 import androidx.compose.ui.text.style.TextAlign
 import androidx.compose.ui.unit.Dp
 import androidx.compose.ui.unit.LayoutDirection
@@ -385,11 +394,24 @@
             date = state.selectedDate,
             calendarModel = state.calendarModel
         )
-        if (formattedDate == null) {
-            Text(getString(string = Strings.DatePickerHeadline), maxLines = 1)
-        } else {
-            Text(formattedDate, maxLines = 1)
-        }
+        val verboseDateDescription = dateFormatter.formatDate(
+            date = state.selectedDate,
+            calendarModel = state.calendarModel,
+            forContentDescription = true
+        ) ?: getString(Strings.DatePickerNoSelectionDescription)
+
+        val headlineText = formattedDate ?: getString(string = Strings.DatePickerHeadline)
+        val headlineDescription =
+            getString(Strings.DatePickerHeadlineDescription).format(verboseDateDescription)
+
+        Text(
+            text = headlineText,
+            modifier = Modifier.semantics {
+                liveRegion = LiveRegionMode.Polite
+                contentDescription = headlineDescription
+            },
+            maxLines = 1
+        )
     }
 
     /**
@@ -645,10 +667,18 @@
     internal fun formatDate(
         date: CalendarDate?,
         calendarModel: CalendarModel,
+        forContentDescription: Boolean = false,
         locale: Locale = Locale.getDefault()
     ): String? {
         if (date == null) return null
-        return calendarModel.formatWithSkeleton(date, selectedDateSkeleton, locale)
+        return calendarModel.formatWithSkeleton(
+            date, if (forContentDescription) {
+                selectedDateDescriptionSkeleton
+            } else {
+                selectedDateSkeleton
+            },
+            locale
+        )
     }
 
     override fun equals(other: Any?): Boolean {
@@ -737,6 +767,7 @@
                     onDateSelected = onDateSelected,
                     datePickerState = datePickerState,
                     lazyListState = monthsListState,
+                    dateFormatter = dateFormatter,
                     dateValidator = dateValidator,
                     colors = colors
                 )
@@ -747,7 +778,12 @@
                 enter = expandVertically() + fadeIn(initialAlpha = 0.6f),
                 exit = shrinkVertically() + fadeOut()
             ) {
-                Column {
+                // Apply a paneTitle to make the screen reader focus on a relevant node after this
+                // column is hidden and disposed.
+                // TODO(b/186443263): Have the screen reader focus on a year in the list when the
+                //  list is revealed.
+                val yearsPaneTitle = getString(Strings.DatePickerYearPickerPaneTitle)
+                Column(modifier = Modifier.semantics { paneTitle = yearsPaneTitle }) {
                     YearPicker(
                         // Keep the height the same as the monthly calendar + weekdays height, and
                         // take into account the thickness of the divider that will be composed
@@ -830,6 +866,7 @@
     onDateSelected: (dateInMillis: Long) -> Unit,
     datePickerState: DatePickerState,
     lazyListState: LazyListState,
+    dateFormatter: DatePickerFormatter,
     dateValidator: (Long) -> Boolean,
     colors: DatePickerColors,
 ) {
@@ -841,6 +878,12 @@
         )
     }
     LazyRow(
+        // Apply this to prevent the screen reader from scrolling to the next or previous month, and
+        // instead, traverse outside the Month composable when swiping from a focused first or last
+        // day of the month.
+        modifier = Modifier.semantics {
+            horizontalScrollAxisRange = ScrollAxisRange(value = { 0f }, maxValue = { 0f })
+        },
         state = lazyListState,
         // TODO(b/264687693): replace with the framework's rememberSnapFlingBehavior(lazyListState)
         //  when promoted to stable
@@ -860,6 +903,7 @@
                     onDateSelected = onDateSelected,
                     today = today,
                     selectedDate = datePickerState.selectedDate,
+                    dateFormatter = dateFormatter,
                     dateValidator = dateValidator,
                     colors = colors
                 )
@@ -946,6 +990,7 @@
     today: CalendarDate,
     selectedDate: CalendarDate?,
     dateValidator: (Long) -> Boolean,
+    dateFormatter: DatePickerFormatter,
     colors: DatePickerColors
 ) {
     ProvideTextStyle(
@@ -982,16 +1027,29 @@
                             val dateInMillis = month.startUtcTimeMillis +
                                 (dayNumber * MillisecondsIn24Hours)
                             Day(
-                                checked = dateInMillis == selectedDate?.utcTimeMillis,
-                                onCheckedChange = { onDateSelected(dateInMillis) },
+                                modifier = Modifier.semantics { role = Role.Button },
+                                selected = dateInMillis == selectedDate?.utcTimeMillis,
+                                onClick = { onDateSelected(dateInMillis) },
                                 animateChecked = true,
                                 enabled = remember(dateInMillis) {
                                     dateValidator.invoke(dateInMillis)
                                 },
                                 today = dateInMillis == today.utcTimeMillis,
-                                text = (dayNumber + 1).toLocalString(),
                                 colors = colors
-                            )
+                            ) {
+                                Text(
+                                    text = (dayNumber + 1).toLocalString(),
+                                    modifier = Modifier.semantics {
+                                        contentDescription =
+                                            formatWithSkeleton(
+                                                dateInMillis,
+                                                dateFormatter.selectedDateDescriptionSkeleton,
+                                                Locale.getDefault()
+                                            )
+                                    },
+                                    textAlign = TextAlign.Center
+                                )
+                            }
                         }
                         cellsCount++
                     }
@@ -1004,18 +1062,19 @@
 @OptIn(ExperimentalMaterial3Api::class)
 @Composable
 private fun Day(
-    checked: Boolean,
-    onCheckedChange: (Boolean) -> Unit,
+    modifier: Modifier,
+    selected: Boolean,
+    onClick: () -> Unit,
     animateChecked: Boolean,
     enabled: Boolean,
     today: Boolean,
-    text: String,
-    colors: DatePickerColors
+    colors: DatePickerColors,
+    content: @Composable () -> Unit
 ) {
     Surface(
-        checked = checked,
-        onCheckedChange = onCheckedChange,
-        modifier = Modifier
+        selected = selected,
+        onClick = onClick,
+        modifier = modifier
             .minimumInteractiveComponentSize()
             .requiredSize(
                 DatePickerModalTokens.DateStateLayerWidth,
@@ -1024,16 +1083,16 @@
         enabled = enabled,
         shape = DatePickerModalTokens.DateContainerShape.toShape(),
         color = colors.dayContainerColor(
-            selected = checked,
+            selected = selected,
             enabled = enabled,
             animate = animateChecked
         ).value,
         contentColor = colors.dayContentColor(
             today = today,
-            selected = checked,
+            selected = selected,
             enabled = enabled,
         ).value,
-        border = if (today && !checked) {
+        border = if (today && !selected) {
             BorderStroke(
                 DatePickerModalTokens.DateTodayContainerOutlineWidth,
                 colors.todayDateBorderColor
@@ -1043,10 +1102,7 @@
         }
     ) {
         Box(contentAlignment = Alignment.Center) {
-            Text(
-                text = text,
-                textAlign = TextAlign.Center
-            )
+            content()
         }
     }
 }
@@ -1080,7 +1136,13 @@
         }
         LazyVerticalGrid(
             columns = GridCells.Fixed(YearsInRow),
-            modifier = modifier.background(containerColor),
+            modifier = modifier
+                .background(containerColor)
+                // Apply this to have the screen reader traverse outside the visible list of years
+                // and not scroll them by default.
+                .semantics {
+                    verticalScrollAxisRange = ScrollAxisRange(value = { 0f }, maxValue = { 0f })
+                },
             state = lazyGridState,
             horizontalArrangement = Arrangement.SpaceEvenly,
             verticalArrangement = Arrangement.spacedBy(YearsVerticalPadding)
@@ -1088,21 +1150,27 @@
             items(datePickerState.yearRange.count()) {
                 val selectedYear = it + datePickerState.yearRange.first
                 Year(
-                    checked = selectedYear == displayedYear,
+                    modifier = Modifier
+                        .requiredSize(
+                            width = DatePickerModalTokens.SelectionYearContainerWidth,
+                            height = DatePickerModalTokens.SelectionYearContainerHeight
+                        )
+                        .semantics {
+                            role = Role.Button
+                        },
+                    selected = selectedYear == displayedYear,
                     currentYear = selectedYear == currentYear,
-                    onCheckedChange = { checked ->
-                        if (checked) {
-                            onYearSelected(selectedYear)
-                        }
-                    },
-                    modifier = Modifier.requiredSize(
-                        width = DatePickerModalTokens.SelectionYearContainerWidth,
-                        height = DatePickerModalTokens.SelectionYearContainerHeight
-                    ),
+                    onClick = { onYearSelected(selectedYear) },
                     colors = colors
                 ) {
+                    val localizedYear = selectedYear.toLocalString()
+                    val description =
+                        getString(Strings.DatePickerNavigateToYearDescription).format(localizedYear)
                     Text(
-                        text = selectedYear.toLocalString(),
+                        text = localizedYear,
+                        modifier = Modifier.semantics {
+                            contentDescription = description
+                        },
                         textAlign = TextAlign.Center
                     )
                 }
@@ -1114,15 +1182,15 @@
 @OptIn(ExperimentalMaterial3Api::class)
 @Composable
 private fun Year(
-    checked: Boolean,
-    currentYear: Boolean,
-    onCheckedChange: (Boolean) -> Unit,
     modifier: Modifier,
+    selected: Boolean,
+    currentYear: Boolean,
+    onClick: () -> Unit,
     colors: DatePickerColors,
     content: @Composable () -> Unit
 ) {
-    val border = remember(currentYear, checked) {
-        if (currentYear && !checked) {
+    val border = remember(currentYear, selected) {
+        if (currentYear && !selected) {
             // Use the day's spec to draw a border around the current year.
             BorderStroke(
                 DatePickerModalTokens.DateTodayContainerOutlineWidth,
@@ -1133,14 +1201,14 @@
         }
     }
     Surface(
-        checked = checked,
-        onCheckedChange = onCheckedChange,
+        selected = selected,
+        onClick = onClick,
         modifier = modifier,
         shape = DatePickerModalTokens.SelectionYearStateLayerShape.toShape(),
-        color = colors.yearContainerColor(selected = checked).value,
+        color = colors.yearContainerColor(selected = selected).value,
         contentColor = colors.yearContentColor(
             currentYear = currentYear,
-            selected = checked
+            selected = selected
         ).value,
         border = border
     ) {
@@ -1180,7 +1248,13 @@
             onClick = onYearPickerButtonClicked,
             expanded = yearPickerVisible
         ) {
-            Text(yearPickerText)
+            Text(text = yearPickerText,
+                modifier = Modifier.semantics {
+                    // Make the screen reader read out updates to the menu button text as the user
+                    // navigates the arrows or scrolls to change the displayed month.
+                    liveRegion = LiveRegionMode.Polite
+                    contentDescription = yearPickerText
+                })
         }
         // Show arrows for traversing months (only visible when the year selection is off)
         if (!yearPickerVisible) {
diff --git a/compose/material3/material3/src/commonMain/kotlin/androidx/compose/material3/InteractiveComponentSize.kt b/compose/material3/material3/src/commonMain/kotlin/androidx/compose/material3/InteractiveComponentSize.kt
index 18ac974..4597f577 100644
--- a/compose/material3/material3/src/commonMain/kotlin/androidx/compose/material3/InteractiveComponentSize.kt
+++ b/compose/material3/material3/src/commonMain/kotlin/androidx/compose/material3/InteractiveComponentSize.kt
@@ -75,6 +75,27 @@
 val LocalMinimumInteractiveComponentEnforcement: ProvidableCompositionLocal<Boolean> =
     staticCompositionLocalOf { true }
 
+/**
+ * CompositionLocal that configures whether Material components that have a visual size that is
+ * lower than the minimum touch target size for accessibility (such as [Button]) will include
+ * extra space outside the component to ensure that they are accessible. If set to false there
+ * will be no extra space, and so it is possible that if the component is placed near the edge of
+ * a layout / near to another component without any padding, there will not be enough space for
+ * an accessible touch target.
+ */
+@Suppress("OPT_IN_MARKER_ON_WRONG_TARGET")
+@get:ExperimentalMaterial3Api
+@ExperimentalMaterial3Api
+@Deprecated(
+    message = "Use LocalMinimumInteractiveComponentEnforcement instead.",
+    replaceWith = ReplaceWith(
+        "LocalMinimumInteractiveComponentEnforcement"
+    ),
+    level = DeprecationLevel.WARNING
+)
+val LocalMinimumTouchTargetEnforcement: ProvidableCompositionLocal<Boolean> =
+    LocalMinimumInteractiveComponentEnforcement
+
 private class MinimumInteractiveComponentSizeModifier(val size: DpSize) : LayoutModifier {
     override fun MeasureScope.measure(
         measurable: Measurable,
diff --git a/compose/material3/material3/src/commonMain/kotlin/androidx/compose/material3/Strings.kt b/compose/material3/material3/src/commonMain/kotlin/androidx/compose/material3/Strings.kt
index b4a1da0..0d58497 100644
--- a/compose/material3/material3/src/commonMain/kotlin/androidx/compose/material3/Strings.kt
+++ b/compose/material3/material3/src/commonMain/kotlin/androidx/compose/material3/Strings.kt
@@ -38,10 +38,14 @@
         val SuggestionsAvailable = Strings(12)
         val DatePickerTitle = Strings(13)
         val DatePickerHeadline = Strings(14)
-        val DatePickerSwitchToYearSelection = Strings(15)
-        val DatePickerSwitchToDaySelection = Strings(16)
-        val DatePickerSwitchToNextMonth = Strings(17)
-        val DatePickerSwitchToPreviousMonth = Strings(18)
+        val DatePickerYearPickerPaneTitle = Strings(15)
+        val DatePickerSwitchToYearSelection = Strings(16)
+        val DatePickerSwitchToDaySelection = Strings(17)
+        val DatePickerSwitchToNextMonth = Strings(18)
+        val DatePickerSwitchToPreviousMonth = Strings(19)
+        val DatePickerNavigateToYearDescription = Strings(20)
+        val DatePickerHeadlineDescription = Strings(21)
+        val DatePickerNoSelectionDescription = Strings(22)
     }
 }
 
diff --git a/compose/material3/material3/src/desktopMain/kotlin/androidx/compose/material/Strings.desktop.kt b/compose/material3/material3/src/desktopMain/kotlin/androidx/compose/material/Strings.desktop.kt
index e2d8ba4..398da25 100644
--- a/compose/material3/material3/src/desktopMain/kotlin/androidx/compose/material/Strings.desktop.kt
+++ b/compose/material3/material3/src/desktopMain/kotlin/androidx/compose/material/Strings.desktop.kt
@@ -35,10 +35,15 @@
         Strings.SuggestionsAvailable -> "Suggestions below"
         Strings.DatePickerTitle -> "Select date"
         Strings.DatePickerHeadline -> "Selected date"
+        Strings.DatePickerYearPickerPaneTitle -> "Year picker visible"
         Strings.DatePickerSwitchToYearSelection -> "Switch to selecting a year"
-        Strings.DatePickerSwitchToDaySelection -> "Switch to selecting a day"
+        Strings.DatePickerSwitchToDaySelection -> "Swipe to select a year, or tap to switch " +
+            "back to selecting a day"
         Strings.DatePickerSwitchToNextMonth -> "Change to next month"
         Strings.DatePickerSwitchToPreviousMonth -> "Change to previous month"
+        Strings.DatePickerNavigateToYearDescription -> "Navigate to year %1$"
+        Strings.DatePickerHeadlineDescription -> "Current selection: %1$"
+        Strings.DatePickerNoSelectionDescription -> "None"
         else -> ""
     }
 }
diff --git a/compose/runtime/runtime-lint/src/main/java/androidx/compose/runtime/lint/RememberDetector.kt b/compose/runtime/runtime-lint/src/main/java/androidx/compose/runtime/lint/RememberDetector.kt
index a83fc1d..559d08c 100644
--- a/compose/runtime/runtime-lint/src/main/java/androidx/compose/runtime/lint/RememberDetector.kt
+++ b/compose/runtime/runtime-lint/src/main/java/androidx/compose/runtime/lint/RememberDetector.kt
@@ -33,7 +33,7 @@
 import org.jetbrains.uast.UCallExpression
 import java.util.EnumSet
 import org.jetbrains.kotlin.psi.KtCallExpression
-import org.jetbrains.kotlin.psi.KtLambdaArgument
+import org.jetbrains.kotlin.psi.KtLambdaExpression
 import org.jetbrains.uast.UExpression
 import org.jetbrains.uast.toUElementOfType
 
@@ -56,13 +56,20 @@
             }
             sourcePsi is KtCallExpression -> {
                 // Even though the return type is Unit, we should double check if the type of
-                // the last expression of the lambda argument matches.
-                val tailLambda = sourcePsi.valueArguments.lastOrNull() as? KtLambdaArgument
-                val lambda = tailLambda?.getLambdaExpression()
-                val lastExp = lambda?.bodyExpression?.statements?.lastOrNull()
-                val lastExpType = lastExp?.toUElementOfType<UExpression>()?.getExpressionType()
-                // If unresolved (i.e., type error), the expression type will be actually `null`.
-                callExpressionType == lastExpType
+                // the lambda expression matches
+                val calculationParameterIndex = method.parameters.lastIndex
+                val argument = node.getArgumentForParameter(calculationParameterIndex)?.sourcePsi
+                // If the argument is a lambda, check the expression inside
+                if (argument is KtLambdaExpression) {
+                    val lastExp = argument.bodyExpression?.statements?.lastOrNull()
+                    val lastExpType = lastExp?.toUElementOfType<UExpression>()?.getExpressionType()
+                    // If unresolved (i.e., type error), the expression type will be actually `null`
+                    callExpressionType == lastExpType
+                } else {
+                    // Otherwise return true, since it is a reference to something else that is
+                    // unit (such as a variable)
+                    true
+                }
            }
            else -> true
         }
diff --git a/compose/runtime/runtime-lint/src/test/java/androidx/compose/runtime/lint/ComposableCoroutineCreationDetectorTest.kt b/compose/runtime/runtime-lint/src/test/java/androidx/compose/runtime/lint/ComposableCoroutineCreationDetectorTest.kt
index 0e75b02..52d93a5 100644
--- a/compose/runtime/runtime-lint/src/test/java/androidx/compose/runtime/lint/ComposableCoroutineCreationDetectorTest.kt
+++ b/compose/runtime/runtime-lint/src/test/java/androidx/compose/runtime/lint/ComposableCoroutineCreationDetectorTest.kt
@@ -19,7 +19,7 @@
 package androidx.compose.runtime.lint
 
 import androidx.compose.lint.test.Stubs
-import androidx.compose.lint.test.compiledStub
+import androidx.compose.lint.test.bytecodeStub
 import com.android.tools.lint.checks.infrastructure.LintDetectorTest
 import com.android.tools.lint.checks.infrastructure.TestFile
 import com.android.tools.lint.checks.infrastructure.TestMode
@@ -41,7 +41,7 @@
     override fun getIssues(): MutableList<Issue> =
         mutableListOf(ComposableCoroutineCreationDetector.CoroutineCreationDuringComposition)
 
-    private val coroutineBuildersStub: TestFile = compiledStub(
+    private val coroutineBuildersStub: TestFile = bytecodeStub(
         filename = "Builders.common.kt",
         filepath = "kotlinx/coroutines",
         checksum = 0xdb1ff08e,
@@ -99,7 +99,7 @@
         """
     )
 
-    private val flowStub: TestFile = compiledStub(
+    private val flowStub: TestFile = bytecodeStub(
         filename = "Flow.kt",
         filepath = "kotlinx/coroutines/flow",
         checksum = 0x3416a857,
@@ -129,7 +129,7 @@
         """
     )
 
-    private val flowBuildersStub: TestFile = compiledStub(
+    private val flowBuildersStub: TestFile = bytecodeStub(
         filename = "Builders.kt",
         filepath = "kotlinx/coroutines/flow",
         checksum = 0xb581dd7,
@@ -164,7 +164,7 @@
         """
     )
 
-    private val flowCollectStub: TestFile = compiledStub(
+    private val flowCollectStub: TestFile = bytecodeStub(
         filename = "Collect.kt",
         filepath = "kotlinx/coroutines/flow",
         checksum = 0x8685bc57,
diff --git a/compose/runtime/runtime-lint/src/test/java/androidx/compose/runtime/lint/ComposableFlowOperatorDetectorTest.kt b/compose/runtime/runtime-lint/src/test/java/androidx/compose/runtime/lint/ComposableFlowOperatorDetectorTest.kt
index 2e48806..58661be 100644
--- a/compose/runtime/runtime-lint/src/test/java/androidx/compose/runtime/lint/ComposableFlowOperatorDetectorTest.kt
+++ b/compose/runtime/runtime-lint/src/test/java/androidx/compose/runtime/lint/ComposableFlowOperatorDetectorTest.kt
@@ -19,7 +19,7 @@
 package androidx.compose.runtime.lint
 
 import androidx.compose.lint.test.Stubs
-import androidx.compose.lint.test.compiledStub
+import androidx.compose.lint.test.bytecodeStub
 import com.android.tools.lint.checks.infrastructure.LintDetectorTest
 import com.android.tools.lint.checks.infrastructure.TestFile
 import com.android.tools.lint.checks.infrastructure.TestMode
@@ -44,7 +44,7 @@
     /**
      * Combined stub of some Flow APIs
      */
-    private val flowStub: TestFile = compiledStub(
+    private val flowStub: TestFile = bytecodeStub(
         filename = "Flow.kt",
         filepath = "kotlinx/coroutines/flow",
         checksum = 0x8d13620c,
diff --git a/compose/runtime/runtime-lint/src/test/java/androidx/compose/runtime/lint/ComposableStateFlowValueDetectorTest.kt b/compose/runtime/runtime-lint/src/test/java/androidx/compose/runtime/lint/ComposableStateFlowValueDetectorTest.kt
index cf2ec2d..6bf3c0a 100644
--- a/compose/runtime/runtime-lint/src/test/java/androidx/compose/runtime/lint/ComposableStateFlowValueDetectorTest.kt
+++ b/compose/runtime/runtime-lint/src/test/java/androidx/compose/runtime/lint/ComposableStateFlowValueDetectorTest.kt
@@ -19,7 +19,7 @@
 package androidx.compose.runtime.lint
 
 import androidx.compose.lint.test.Stubs
-import androidx.compose.lint.test.compiledStub
+import androidx.compose.lint.test.bytecodeStub
 import com.android.tools.lint.checks.infrastructure.LintDetectorTest
 import com.android.tools.lint.checks.infrastructure.TestFile
 import com.android.tools.lint.checks.infrastructure.TestMode
@@ -44,7 +44,7 @@
     /**
      * Combined stub of StateFlow / supertypes
      */
-    private val stateFlowStub: TestFile = compiledStub(
+    private val stateFlowStub: TestFile = bytecodeStub(
         filename = "StateFlow.kt",
         filepath = "kotlinx/coroutines/flow",
         checksum = 0x5f478927,
diff --git a/compose/runtime/runtime-lint/src/test/java/androidx/compose/runtime/lint/CompositionLocalNamingDetectorTest.kt b/compose/runtime/runtime-lint/src/test/java/androidx/compose/runtime/lint/CompositionLocalNamingDetectorTest.kt
index 1f39ba8..fcb8bbf 100644
--- a/compose/runtime/runtime-lint/src/test/java/androidx/compose/runtime/lint/CompositionLocalNamingDetectorTest.kt
+++ b/compose/runtime/runtime-lint/src/test/java/androidx/compose/runtime/lint/CompositionLocalNamingDetectorTest.kt
@@ -18,7 +18,7 @@
 
 package androidx.compose.runtime.lint
 
-import androidx.compose.lint.test.compiledStub
+import androidx.compose.lint.test.bytecodeStub
 import com.android.tools.lint.checks.infrastructure.LintDetectorTest
 import com.android.tools.lint.detector.api.Detector
 import com.android.tools.lint.detector.api.Issue
@@ -39,7 +39,7 @@
         mutableListOf(CompositionLocalNamingDetector.CompositionLocalNaming)
 
     // Simplified CompositionLocal.kt stubs
-    private val compositionLocalStub = compiledStub(
+    private val compositionLocalStub = bytecodeStub(
         filename = "CompositionLocal.kt",
         filepath = "androidx/compose/runtime",
         checksum = 0x48f5c823,
diff --git a/compose/runtime/runtime-lint/src/test/java/androidx/compose/runtime/lint/MutableCollectionMutableStateDetectorTest.kt b/compose/runtime/runtime-lint/src/test/java/androidx/compose/runtime/lint/MutableCollectionMutableStateDetectorTest.kt
index 9d57ef0..114fae4 100644
--- a/compose/runtime/runtime-lint/src/test/java/androidx/compose/runtime/lint/MutableCollectionMutableStateDetectorTest.kt
+++ b/compose/runtime/runtime-lint/src/test/java/androidx/compose/runtime/lint/MutableCollectionMutableStateDetectorTest.kt
@@ -19,7 +19,7 @@
 package androidx.compose.runtime.lint
 
 import androidx.compose.lint.test.Stubs
-import androidx.compose.lint.test.kotlinAndCompiledStub
+import androidx.compose.lint.test.kotlinAndBytecodeStub
 import com.android.tools.lint.checks.infrastructure.LintDetectorTest
 import com.android.tools.lint.checks.infrastructure.TestMode
 import com.android.tools.lint.detector.api.Detector
@@ -43,7 +43,7 @@
     /**
      * Extensions / subclasses around Kotlin mutable collections, both in source and compiled form.
      */
-    private val KotlinMutableCollectionExtensions = kotlinAndCompiledStub(
+    private val KotlinMutableCollectionExtensions = kotlinAndBytecodeStub(
         filename = "MutableCollectionExtensions.kt",
         filepath = "stubs",
         checksum = 0x90938fc8,
@@ -462,7 +462,7 @@
      * Extensions / subclasses around Kotlin immutable collections, both in source and compiled
      * form.
      */
-    private val KotlinImmutableCollectionExtensions = kotlinAndCompiledStub(
+    private val KotlinImmutableCollectionExtensions = kotlinAndBytecodeStub(
         filename = "ImmutableCollectionExtensions.kt",
         filepath = "stubs",
         checksum = 0xf50711c2,
@@ -1400,7 +1400,7 @@
             ),
             Stubs.SnapshotState,
             Stubs.Composable,
-            KotlinMutableCollectionExtensions.compiled
+            KotlinMutableCollectionExtensions.bytecode
         )
             .skipTestModes(TestMode.TYPE_ALIAS)
             .run()
@@ -1576,7 +1576,7 @@
             ),
             Stubs.SnapshotState,
             Stubs.Composable,
-            KotlinImmutableCollectionExtensions.compiled
+            KotlinImmutableCollectionExtensions.bytecode
         )
             .skipTestModes(TestMode.TYPE_ALIAS)
             .run()
diff --git a/compose/runtime/runtime-lint/src/test/java/androidx/compose/runtime/lint/RememberDetectorTest.kt b/compose/runtime/runtime-lint/src/test/java/androidx/compose/runtime/lint/RememberDetectorTest.kt
index dfbd1b4..937ec62 100644
--- a/compose/runtime/runtime-lint/src/test/java/androidx/compose/runtime/lint/RememberDetectorTest.kt
+++ b/compose/runtime/runtime-lint/src/test/java/androidx/compose/runtime/lint/RememberDetectorTest.kt
@@ -61,6 +61,9 @@
                     val unit = remember {
                         state.update(5)
                     }
+                    val unitLambda: () -> Unit = {}
+                    remember(unitLambda)
+                    val unit2 = remember(unitLambda)
                 }
 
                 @Composable
@@ -72,6 +75,9 @@
                     val unit = remember(number) {
                         state.update(number)
                     }
+                    val unitLambda: () -> Unit = {}
+                    remember(number, unitLambda)
+                    val unit2 = remember(number, unitLambda)
                 }
 
                 @Composable
@@ -85,6 +91,9 @@
                         state.update(number1)
                         state.update(number2)
                     }
+                    val unitLambda: () -> Unit = {}
+                    remember(number1, number2, unitLambda)
+                    val unit2 = remember(number1, number2, unitLambda)
                 }
 
                 @Composable
@@ -100,6 +109,9 @@
                         state.update(number2)
                         state.update(number3)
                     }
+                    val unitLambda: () -> Unit = {}
+                    remember(number1, number2, number3, unitLambda)
+                    val unit2 = remember(number1, number2, number3, unitLambda)
                 }
 
                 @Composable
@@ -115,6 +127,9 @@
                         state.update(number2)
                         state.update(number3)
                     }
+                    val unitLambda: () -> Unit = {}
+                    remember(number1, number2, number3, flag, calculation = unitLambda)
+                    val unit2 = remember(number1, number2, number3, flag, calculation = unitLambda)
                 }
             """
             ),
@@ -130,31 +145,61 @@
 src/androidx/compose/runtime/foo/FooState.kt:17: Error: remember calls must not return Unit [RememberReturnType]
                     val unit = remember {
                                ~~~~~~~~
-src/androidx/compose/runtime/foo/FooState.kt:25: Error: remember calls must not return Unit [RememberReturnType]
+src/androidx/compose/runtime/foo/FooState.kt:21: Error: remember calls must not return Unit [RememberReturnType]
+                    remember(unitLambda)
+                    ~~~~~~~~
+src/androidx/compose/runtime/foo/FooState.kt:22: Error: remember calls must not return Unit [RememberReturnType]
+                    val unit2 = remember(unitLambda)
+                                ~~~~~~~~
+src/androidx/compose/runtime/foo/FooState.kt:28: Error: remember calls must not return Unit [RememberReturnType]
                     remember(number) {
                     ~~~~~~~~
-src/androidx/compose/runtime/foo/FooState.kt:28: Error: remember calls must not return Unit [RememberReturnType]
+src/androidx/compose/runtime/foo/FooState.kt:31: Error: remember calls must not return Unit [RememberReturnType]
                     val unit = remember(number) {
                                ~~~~~~~~
+src/androidx/compose/runtime/foo/FooState.kt:35: Error: remember calls must not return Unit [RememberReturnType]
+                    remember(number, unitLambda)
+                    ~~~~~~~~
 src/androidx/compose/runtime/foo/FooState.kt:36: Error: remember calls must not return Unit [RememberReturnType]
+                    val unit2 = remember(number, unitLambda)
+                                ~~~~~~~~
+src/androidx/compose/runtime/foo/FooState.kt:42: Error: remember calls must not return Unit [RememberReturnType]
                     remember(number1, number2) {
                     ~~~~~~~~
-src/androidx/compose/runtime/foo/FooState.kt:40: Error: remember calls must not return Unit [RememberReturnType]
+src/androidx/compose/runtime/foo/FooState.kt:46: Error: remember calls must not return Unit [RememberReturnType]
                     val unit = remember(number1, number2) {
                                ~~~~~~~~
-src/androidx/compose/runtime/foo/FooState.kt:49: Error: remember calls must not return Unit [RememberReturnType]
+src/androidx/compose/runtime/foo/FooState.kt:51: Error: remember calls must not return Unit [RememberReturnType]
+                    remember(number1, number2, unitLambda)
+                    ~~~~~~~~
+src/androidx/compose/runtime/foo/FooState.kt:52: Error: remember calls must not return Unit [RememberReturnType]
+                    val unit2 = remember(number1, number2, unitLambda)
+                                ~~~~~~~~
+src/androidx/compose/runtime/foo/FooState.kt:58: Error: remember calls must not return Unit [RememberReturnType]
                     remember(number1, number2, number3) {
                     ~~~~~~~~
-src/androidx/compose/runtime/foo/FooState.kt:54: Error: remember calls must not return Unit [RememberReturnType]
+src/androidx/compose/runtime/foo/FooState.kt:63: Error: remember calls must not return Unit [RememberReturnType]
                     val unit = remember(number1, number2, number3) {
                                ~~~~~~~~
-src/androidx/compose/runtime/foo/FooState.kt:64: Error: remember calls must not return Unit [RememberReturnType]
+src/androidx/compose/runtime/foo/FooState.kt:69: Error: remember calls must not return Unit [RememberReturnType]
+                    remember(number1, number2, number3, unitLambda)
+                    ~~~~~~~~
+src/androidx/compose/runtime/foo/FooState.kt:70: Error: remember calls must not return Unit [RememberReturnType]
+                    val unit2 = remember(number1, number2, number3, unitLambda)
+                                ~~~~~~~~
+src/androidx/compose/runtime/foo/FooState.kt:76: Error: remember calls must not return Unit [RememberReturnType]
                     remember(number1, number2, number3, flag) {
                     ~~~~~~~~
-src/androidx/compose/runtime/foo/FooState.kt:69: Error: remember calls must not return Unit [RememberReturnType]
+src/androidx/compose/runtime/foo/FooState.kt:81: Error: remember calls must not return Unit [RememberReturnType]
                     val unit = remember(number1, number2, number3, flag) {
                                ~~~~~~~~
-10 errors, 0 warnings
+src/androidx/compose/runtime/foo/FooState.kt:87: Error: remember calls must not return Unit [RememberReturnType]
+                    remember(number1, number2, number3, flag, calculation = unitLambda)
+                    ~~~~~~~~
+src/androidx/compose/runtime/foo/FooState.kt:88: Error: remember calls must not return Unit [RememberReturnType]
+                    val unit2 = remember(number1, number2, number3, flag, calculation = unitLambda)
+                                ~~~~~~~~
+20 errors, 0 warnings
             """
             )
     }
diff --git a/compose/runtime/runtime-saveable-lint/src/test/java/androidx/compose/runtime/saveable/lint/RememberSaveableDetectorTest.kt b/compose/runtime/runtime-saveable-lint/src/test/java/androidx/compose/runtime/saveable/lint/RememberSaveableDetectorTest.kt
index ffb6fa2b..15ce6b5 100644
--- a/compose/runtime/runtime-saveable-lint/src/test/java/androidx/compose/runtime/saveable/lint/RememberSaveableDetectorTest.kt
+++ b/compose/runtime/runtime-saveable-lint/src/test/java/androidx/compose/runtime/saveable/lint/RememberSaveableDetectorTest.kt
@@ -19,7 +19,7 @@
 package androidx.compose.runtime.saveable.lint
 
 import androidx.compose.lint.test.Stubs
-import androidx.compose.lint.test.compiledStub
+import androidx.compose.lint.test.bytecodeStub
 import com.android.tools.lint.checks.infrastructure.LintDetectorTest
 import com.android.tools.lint.checks.infrastructure.TestFile
 import com.android.tools.lint.detector.api.Detector
@@ -39,7 +39,7 @@
     override fun getIssues(): MutableList<Issue> =
         mutableListOf(RememberSaveableDetector.RememberSaveableSaverParameter)
 
-    private val rememberSaveableStub: TestFile = compiledStub(
+    private val rememberSaveableStub: TestFile = bytecodeStub(
         filename = "RememberSaveable.kt",
         filepath = "androidx/compose/runtime/saveable",
         checksum = 0x90b6d5a7,
diff --git a/compose/runtime/runtime/api/current.txt b/compose/runtime/runtime/api/current.txt
index b885979..20f83f5eb 100644
--- a/compose/runtime/runtime/api/current.txt
+++ b/compose/runtime/runtime/api/current.txt
@@ -576,6 +576,9 @@
 
 package androidx.compose.runtime.collection {
 
+  public final class ActualIntMap_androidKt {
+  }
+
   public final class MutableVector<T> implements java.util.RandomAccess {
     method public boolean add(T? element);
     method public void add(int index, T? element);
diff --git a/compose/runtime/runtime/api/public_plus_experimental_current.txt b/compose/runtime/runtime/api/public_plus_experimental_current.txt
index 969339d..20810fc 100644
--- a/compose/runtime/runtime/api/public_plus_experimental_current.txt
+++ b/compose/runtime/runtime/api/public_plus_experimental_current.txt
@@ -625,6 +625,9 @@
 
 package androidx.compose.runtime.collection {
 
+  public final class ActualIntMap_androidKt {
+  }
+
   public final class MutableVector<T> implements java.util.RandomAccess {
     method public boolean add(T? element);
     method public void add(int index, T? element);
diff --git a/compose/runtime/runtime/api/restricted_current.txt b/compose/runtime/runtime/api/restricted_current.txt
index bd0e1a0..3cc2fcc 100644
--- a/compose/runtime/runtime/api/restricted_current.txt
+++ b/compose/runtime/runtime/api/restricted_current.txt
@@ -603,6 +603,9 @@
 
 package androidx.compose.runtime.collection {
 
+  public final class ActualIntMap_androidKt {
+  }
+
   public final class MutableVector<T> implements java.util.RandomAccess {
     ctor @kotlin.PublishedApi internal MutableVector(@kotlin.PublishedApi T![] content, int size);
     method public boolean add(T? element);
diff --git a/compose/runtime/runtime/src/androidAndroidTest/kotlin/androidx/compose/runtime/collection/SparseArrayImplTest.kt b/compose/runtime/runtime/src/androidAndroidTest/kotlin/androidx/compose/runtime/collection/SparseArrayImplTest.kt
new file mode 100644
index 0000000..89f3bd6
--- /dev/null
+++ b/compose/runtime/runtime/src/androidAndroidTest/kotlin/androidx/compose/runtime/collection/SparseArrayImplTest.kt
@@ -0,0 +1,95 @@
+/*
+ * Copyright 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR 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.collection
+
+import kotlin.random.Random
+import kotlin.test.Test
+import kotlin.test.assertEquals
+
+class SparseArrayImplTest {
+    @Test
+    fun addingValue_increasesSize() {
+        val subject = IntMap<Int>(10)
+        subject[1] = 2
+        assertEquals(subject.size, 1)
+
+        subject[2] = 3
+        assertEquals(subject.size, 2)
+
+        subject[1] = 5
+        assertEquals(subject.size, 2)
+    }
+
+    @Test
+    fun setValue_canBeRetrieved() {
+        val subject = IntMap<Int>(10)
+        val added = mutableSetOf<Int>()
+        for (i in 1..1000) {
+            val next = Random.nextInt(i)
+            added.add(next)
+            subject[next] = next
+        }
+        for (item in added) {
+            assertEquals(subject[item], item)
+        }
+    }
+
+    @Test
+    fun removingValue_decreasesSize() {
+        val (subject, added) = makeRandom100()
+        val item = added.first()
+        subject.remove(item)
+        assertEquals(subject.size, added.size - 1)
+        assertEquals(subject[item], null)
+    }
+
+    @Test
+    fun removedValue_canBeSet() {
+        val (subject, added) = makeRandom100()
+        val item = added.first()
+        subject.remove(item)
+        subject[item] = -1
+        assertEquals(subject[item], -1)
+    }
+
+    @Test
+    fun clear_clears() {
+        val (subject, added) = makeRandom100()
+        subject.clear()
+        for (item in added) {
+            assertEquals(subject[item], null)
+        }
+
+        val item = added.first()
+        subject[item] = -1
+        assertEquals(subject[item], -1)
+    }
+
+    private fun makeRandom100(): Pair<IntMap<Int>, MutableSet<Int>> {
+        val subject = IntMap<Int>(10)
+        val added = mutableSetOf<Int>()
+        for (i in 1..1000) {
+            val next = Random.nextInt(i)
+            added.add(next)
+            subject[next] = next
+        }
+        for (item in added) {
+            assertEquals(subject[item], item)
+        }
+        return subject to added
+    }
+}
\ No newline at end of file
diff --git a/compose/runtime/runtime/src/androidMain/kotlin/androidx/compose/runtime/collection/ActualIntMap.android.kt b/compose/runtime/runtime/src/androidMain/kotlin/androidx/compose/runtime/collection/ActualIntMap.android.kt
new file mode 100644
index 0000000..c8bd256
--- /dev/null
+++ b/compose/runtime/runtime/src/androidMain/kotlin/androidx/compose/runtime/collection/ActualIntMap.android.kt
@@ -0,0 +1,66 @@
+/*
+ * Copyright 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.compose.runtime.collection
+
+import android.util.SparseArray
+
+internal actual class IntMap<E> private constructor(
+    private val sparseArray: android.util.SparseArray<E>
+) {
+    constructor(initialCapacity: Int = 10) : this(SparseArray(initialCapacity))
+
+    /**
+     * True if this map contains key
+     */
+    actual operator fun contains(key: Int): Boolean = sparseArray.indexOfKey(key) >= 0
+
+    /**
+     * Get [key] or null
+     */
+    actual operator fun get(key: Int): E? = sparseArray[key]
+
+    /**
+     * Get [key] or [valueIfAbsent]
+     */
+    actual fun get(key: Int, valueIfAbsent: E): E = sparseArray.get(key, valueIfAbsent)
+
+    /**
+     * Set [key] to [value]
+     */
+    actual operator fun set(key: Int, value: E) = sparseArray.put(key, value)
+
+    /**
+     * Remove key, if it exists
+     *
+     * Otherwise no op
+     */
+    actual fun remove(key: Int) = sparseArray.remove(key)
+
+    /**
+     * Clear this map
+     */
+    actual fun clear() = sparseArray.clear()
+
+    /**
+     * Current count of values
+     */
+    actual val size: Int
+        get() = sparseArray.size()
+}
+
+// filename is not allowed if only a class is declared
+private val allowFilename = 0
\ No newline at end of file
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 62b849c..9d387be 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
@@ -22,6 +22,7 @@
 import androidx.compose.runtime.Composer.Companion.equals
 import androidx.compose.runtime.collection.IdentityArrayMap
 import androidx.compose.runtime.collection.IdentityArraySet
+import androidx.compose.runtime.collection.IntMap
 import androidx.compose.runtime.external.kotlinx.collections.immutable.PersistentMap
 import androidx.compose.runtime.external.kotlinx.collections.immutable.persistentHashMapOf
 import androidx.compose.runtime.snapshots.currentSnapshot
@@ -1248,7 +1249,7 @@
     private val invalidations: MutableList<Invalidation> = mutableListOf()
     private val entersStack = IntStack()
     private var parentProvider: CompositionLocalMap = persistentHashMapOf()
-    private val providerUpdates = HashMap<Int, CompositionLocalMap>()
+    private val providerUpdates = IntMap<CompositionLocalMap>()
     private var providersInvalid = false
     private val providersInvalidStack = IntStack()
     private var reusing = false
@@ -1885,12 +1886,15 @@
         record { _, _, rememberManager -> rememberManager.sideEffect(effect) }
     }
 
+    private fun currentCompositionLocalScope(): CompositionLocalMap {
+        providerCache?.let { return it }
+        return currentCompositionLocalScope(reader.parent)
+    }
+
     /**
      * Return the current [CompositionLocal] scope which was provided by a parent group.
      */
-    private fun currentCompositionLocalScope(group: Int? = null): CompositionLocalMap {
-        if (group == null)
-            providerCache?.let { return it }
+    private fun currentCompositionLocalScope(group: Int): CompositionLocalMap {
         if (inserting && writerHasAProvider) {
             var current = writer.parent
             while (current > 0) {
@@ -1906,7 +1910,7 @@
             }
         }
         if (reader.size > 0) {
-            var current = group ?: reader.parent
+            var current = group
             while (current > 0) {
                 if (reader.groupKey(current) == compositionLocalMapKey &&
                     reader.groupObjectKey(current) == compositionLocalMap
diff --git a/compose/runtime/runtime/src/commonMain/kotlin/androidx/compose/runtime/collection/IntMap.kt b/compose/runtime/runtime/src/commonMain/kotlin/androidx/compose/runtime/collection/IntMap.kt
new file mode 100644
index 0000000..c53ec51
--- /dev/null
+++ b/compose/runtime/runtime/src/commonMain/kotlin/androidx/compose/runtime/collection/IntMap.kt
@@ -0,0 +1,59 @@
+/*
+ * Copyright 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.compose.runtime.collection
+
+/**
+ * Map of (int) -> Element that attempts to avoid boxing.
+ */
+internal expect class IntMap<E> {
+    /**
+     * True if this map contains key
+     */
+    operator fun contains(key: Int): Boolean
+
+    /**
+     * Get [key] or null
+     */
+    operator fun get(key: Int): E?
+
+    /**
+     * Get [key] or [valueIfAbsent]
+     */
+    fun get(key: Int, valueIfAbsent: E): E
+
+    /**
+     * Sets [key] to [value]
+     */
+    operator fun set(key: Int, value: E)
+
+    /**
+     * Remove [key], if it exists
+     *
+     * Otherwise no op
+     */
+    fun remove(key: Int)
+
+    /**
+     * Clear this map
+     */
+    fun clear()
+
+    /**
+     * Current count of key value pairs.
+     */
+    val size: Int
+}
\ No newline at end of file
diff --git a/compose/runtime/runtime/src/desktopMain/kotlin/androidx/compose/runtime/collection/ActualIntMap.desktop.kt b/compose/runtime/runtime/src/desktopMain/kotlin/androidx/compose/runtime/collection/ActualIntMap.desktop.kt
new file mode 100644
index 0000000..5b2d961
--- /dev/null
+++ b/compose/runtime/runtime/src/desktopMain/kotlin/androidx/compose/runtime/collection/ActualIntMap.desktop.kt
@@ -0,0 +1,153 @@
+/*
+ * Copyright 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR 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.collection
+
+internal actual class IntMap<T> {
+    private val DELETED = Any()
+    private var keys = IntArray(10)
+    private var _size = 0
+    private var values = Array<Any?>(10) { null }
+
+    /**
+     * True if this map contains key
+     */
+    actual operator fun contains(key: Int): Boolean {
+        return keys.binarySearch(_size, key) >= 0
+    }
+
+    /**
+     * Get [key] or null
+     */
+    actual operator fun get(key: Int): T? {
+        val index = keys.binarySearch(_size, key)
+        return if (index >= 0 && values[index] !== DELETED) {
+            @Suppress("UNCHECKED_CAST")
+            values[index] as T
+        } else {
+            null
+        }
+    }
+
+    /**
+     * Get [key] or [valueIfNotFound]
+     */
+    actual fun get(key: Int, valueIfAbsent: T): T {
+        val index = keys.binarySearch(_size, key)
+        return if (index >= 0 && values[index] !== DELETED) {
+            @Suppress("UNCHECKED_CAST")
+            values[index] as T
+        } else {
+            valueIfAbsent
+        }
+    }
+
+    /**
+     * Set [key] to [value]
+     */
+    actual operator fun set(key: Int, value: T) {
+        var index = keys.binarySearch(_size, key)
+        if (index >= 0) {
+            values[index] = value
+        } else {
+            index = -index
+            keys = keys.insert(_size, index, key)
+            values = values.insert(_size, index, value)
+            _size++
+        }
+    }
+
+    /**
+     * Remove key, if it exists
+     *
+     * Otherwise no op
+     */
+    actual fun remove(key: Int) {
+        // note this never GCs
+        val index = keys.binarySearch(_size, key)
+        if (index >= 0) {
+            values[index] = DELETED
+        }
+    }
+
+    /**
+     * Clear this map
+     */
+    actual fun clear() {
+        _size = 0
+        for (i in keys.indices) {
+            keys[i] = 0
+        }
+        for (i in values.indices) {
+            values[i] = null
+        }
+    }
+
+    /**
+     * Current count of (key, value) pairs
+     */
+    actual val size: Int
+        get() = _size
+}
+
+private fun IntArray.binarySearch(size: Int, value: Int): Int {
+    var max = 0
+    var min = size - 1
+    while (max <= min) {
+        val mid = max + min / 2
+        val midValue = this[mid]
+        if (midValue < value) {
+            max = mid + 1
+        } else if (midValue > value) {
+            min = mid - 1
+        } else {
+            return mid
+        }
+    }
+    return -(max + 1)
+}
+
+private fun IntArray.insert(currentSize: Int, index: Int, value: Int): IntArray {
+    if (currentSize + 1 <= size) {
+        if (index < currentSize) {
+            System.arraycopy(this, index, this, index + 1, currentSize - index)
+        }
+        this[index] = value
+        return this
+    }
+
+    val result = IntArray(size * 2)
+    System.arraycopy(this, 0, result, 0, index)
+    result[index] = value
+    System.arraycopy(this, index, result, index + 1, size - index)
+    return result
+}
+
+private fun Array<Any?>.insert(currentSize: Int, index: Int, value: Any?): Array<Any?> {
+    if (currentSize + 1 <= size) {
+        if (index < currentSize) {
+            System.arraycopy(this, index, this, index + 1, currentSize - index)
+        }
+        this[index] = value
+        return this
+    }
+
+    val result = Array<Any?>(size * 2) { null }
+    System.arraycopy(this, 0, result, 0, index)
+    result[index] = value
+    System.arraycopy(this, index, result, index + 1, size - index)
+    return result
+}
\ No newline at end of file
diff --git a/compose/ui/ui-graphics/api/current.txt b/compose/ui/ui-graphics/api/current.txt
index 8f387eca..fd2b2c0 100644
--- a/compose/ui/ui-graphics/api/current.txt
+++ b/compose/ui/ui-graphics/api/current.txt
@@ -735,7 +735,9 @@
 
   public final class RectHelper_androidKt {
     method public static android.graphics.Rect toAndroidRect(androidx.compose.ui.geometry.Rect);
+    method public static android.graphics.Rect toAndroidRect(androidx.compose.ui.unit.IntRect);
     method public static android.graphics.RectF toAndroidRectF(androidx.compose.ui.geometry.Rect);
+    method public static androidx.compose.ui.unit.IntRect toComposeIntRect(android.graphics.Rect);
     method public static androidx.compose.ui.geometry.Rect toComposeRect(android.graphics.Rect);
   }
 
diff --git a/compose/ui/ui-graphics/api/public_plus_experimental_current.txt b/compose/ui/ui-graphics/api/public_plus_experimental_current.txt
index f5a56d4..b9e2ae2 100644
--- a/compose/ui/ui-graphics/api/public_plus_experimental_current.txt
+++ b/compose/ui/ui-graphics/api/public_plus_experimental_current.txt
@@ -738,7 +738,9 @@
 
   public final class RectHelper_androidKt {
     method public static android.graphics.Rect toAndroidRect(androidx.compose.ui.geometry.Rect);
+    method public static android.graphics.Rect toAndroidRect(androidx.compose.ui.unit.IntRect);
     method public static android.graphics.RectF toAndroidRectF(androidx.compose.ui.geometry.Rect);
+    method public static androidx.compose.ui.unit.IntRect toComposeIntRect(android.graphics.Rect);
     method public static androidx.compose.ui.geometry.Rect toComposeRect(android.graphics.Rect);
   }
 
diff --git a/compose/ui/ui-graphics/api/restricted_current.txt b/compose/ui/ui-graphics/api/restricted_current.txt
index a7f88e5..c59dbea 100644
--- a/compose/ui/ui-graphics/api/restricted_current.txt
+++ b/compose/ui/ui-graphics/api/restricted_current.txt
@@ -767,7 +767,9 @@
 
   public final class RectHelper_androidKt {
     method public static android.graphics.Rect toAndroidRect(androidx.compose.ui.geometry.Rect);
+    method public static android.graphics.Rect toAndroidRect(androidx.compose.ui.unit.IntRect);
     method public static android.graphics.RectF toAndroidRectF(androidx.compose.ui.geometry.Rect);
+    method public static androidx.compose.ui.unit.IntRect toComposeIntRect(android.graphics.Rect);
     method public static androidx.compose.ui.geometry.Rect toComposeRect(android.graphics.Rect);
   }
 
diff --git a/compose/ui/ui-graphics/src/androidAndroidTest/kotlin/androidx/compose/ui/graphics/RectHelperTest.kt b/compose/ui/ui-graphics/src/androidAndroidTest/kotlin/androidx/compose/ui/graphics/RectHelperTest.kt
new file mode 100644
index 0000000..248e171
--- /dev/null
+++ b/compose/ui/ui-graphics/src/androidAndroidTest/kotlin/androidx/compose/ui/graphics/RectHelperTest.kt
@@ -0,0 +1,120 @@
+/*
+ * Copyright 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.compose.ui.graphics
+
+import androidx.compose.ui.geometry.Rect
+import androidx.compose.ui.unit.IntRect
+import androidx.test.filters.SmallTest
+import org.junit.Assert.assertEquals
+import org.junit.Test
+import org.junit.runner.RunWith
+import androidx.test.ext.junit.runners.AndroidJUnit4
+
+@SmallTest
+@RunWith(AndroidJUnit4::class)
+class RectHelperTest {
+
+    @Test
+    fun rectToAndroidRectTruncates() {
+        assertEquals(
+            android.graphics.Rect(
+                2,
+                3,
+                4,
+                5
+            ),
+            Rect(
+                2f,
+                3.1f,
+                4.5f,
+                5.99f
+            ).toAndroidRect()
+        )
+    }
+
+    @Test
+    fun rectToAndroidRectFConverts() {
+        assertEquals(
+            android.graphics.RectF(
+                2f,
+                3.1f,
+                4.5f,
+                5.99f
+            ),
+            Rect(
+                2f,
+                3.1f,
+                4.5f,
+                5.99f
+            ).toAndroidRectF()
+        )
+    }
+
+    @Test
+    fun androidRectToRectConverts() {
+        assertEquals(
+            Rect(
+                2f,
+                3f,
+                4f,
+                5f
+            ),
+            android.graphics.Rect(
+                2,
+                3,
+                4,
+                5
+            ).toComposeRect(),
+        )
+    }
+
+    @Test
+    fun intRectToAndroidRectConverts() {
+        assertEquals(
+            android.graphics.Rect(
+                2,
+                3,
+                4,
+                5
+            ),
+            IntRect(
+                2,
+                3,
+                4,
+                5
+            ).toAndroidRect(),
+        )
+    }
+
+    @Test
+    fun androidRectToIntRectConverts() {
+        assertEquals(
+            IntRect(
+                2,
+                3,
+                4,
+                5
+            ),
+            android.graphics.Rect(
+                2,
+                3,
+                4,
+                5
+            ).toComposeIntRect(),
+        )
+    }
+}
diff --git a/compose/ui/ui-graphics/src/androidMain/kotlin/androidx/compose/ui/graphics/RectHelper.android.kt b/compose/ui/ui-graphics/src/androidMain/kotlin/androidx/compose/ui/graphics/RectHelper.android.kt
index 9a9e1c0..c926c3c 100644
--- a/compose/ui/ui-graphics/src/androidMain/kotlin/androidx/compose/ui/graphics/RectHelper.android.kt
+++ b/compose/ui/ui-graphics/src/androidMain/kotlin/androidx/compose/ui/graphics/RectHelper.android.kt
@@ -16,6 +16,7 @@
 package androidx.compose.ui.graphics
 
 import androidx.compose.ui.geometry.Rect
+import androidx.compose.ui.unit.IntRect
 
 /**
  * Creates a new instance of [android.graphics.Rect] with the same bounds
@@ -53,4 +54,18 @@
         this.top.toFloat(),
         this.right.toFloat(),
         this.bottom.toFloat()
-    )
\ No newline at end of file
+    )
+
+/**
+ * Creates a new instance of [android.graphics.Rect] with the same bounds
+ * specified in the given [IntRect]
+ */
+fun IntRect.toAndroidRect(): android.graphics.Rect =
+    android.graphics.Rect(left, top, right, bottom)
+
+/**
+ * Creates a new instance of [androidx.compose.ui.unit.IntRect] with the same bounds
+ * specified in the given [android.graphics.Rect]
+ */
+fun android.graphics.Rect.toComposeIntRect(): IntRect =
+    IntRect(left, top, right, bottom)
\ No newline at end of file
diff --git a/compose/ui/ui-inspection/src/androidTest/java/androidx/compose/ui/inspection/DialogTest.kt b/compose/ui/ui-inspection/src/androidTest/java/androidx/compose/ui/inspection/DialogTest.kt
index fce8bbd..f0e401e 100644
--- a/compose/ui/ui-inspection/src/androidTest/java/androidx/compose/ui/inspection/DialogTest.kt
+++ b/compose/ui/ui-inspection/src/androidTest/java/androidx/compose/ui/inspection/DialogTest.kt
@@ -52,7 +52,9 @@
         assertThat(appRoots).hasSize(1)
         assertThat(dialogRoots).hasSize(1)
         assertThat(appRoots.single().name).isEqualTo("Column")
+        assertThat(appRoots.single().inlined).isTrue()
         assertThat(dialogRoots.single().name).isEqualTo("AlertDialog")
+        assertThat(dialogRoots.single().inlined).isFalse()
         val location = IntArray(2)
         dialogViewRoot.getLocationOnScreen(location)
         assertThat(dialogRoots.single().left).isEqualTo(location[0])
@@ -77,6 +79,7 @@
                 it.bounds.layout.y + it.bounds.layout.h
             )
             node.children.addAll(it.childrenList.convert(strings))
+            node.inlined = (it.flags and ComposableNode.Flags.INLINED_VALUE) != 0
             node.build()
         }
 }
diff --git a/compose/ui/ui-inspection/src/androidTest/java/androidx/compose/ui/inspection/inspector/LayoutInspectorTreeTest.kt b/compose/ui/ui-inspection/src/androidTest/java/androidx/compose/ui/inspection/inspector/LayoutInspectorTreeTest.kt
index 52ebdd0..c26a84d 100644
--- a/compose/ui/ui-inspection/src/androidTest/java/androidx/compose/ui/inspection/inspector/LayoutInspectorTreeTest.kt
+++ b/compose/ui/ui-inspection/src/androidTest/java/androidx/compose/ui/inspection/inspector/LayoutInspectorTreeTest.kt
@@ -206,7 +206,8 @@
                 name = "Column",
                 fileName = "LayoutInspectorTreeTest.kt",
                 left = 0.0.dp, top = 0.0.dp, width = 100.dp, height = 82.dp,
-                children = listOf("Text", "Icon", "Surface")
+                children = listOf("Text", "Icon", "Surface"),
+                inlined = true,
             )
             node(
                 name = "Text",
@@ -275,7 +276,8 @@
                 hasTransformations = false,
                 fileName = "LayoutInspectorTreeTest.kt",
                 left = 0.dp, top = 0.dp, width = 100.dp, height = 10.dp,
-                children = listOf("Text")
+                children = listOf("Text"),
+                inlined = true,
             )
             node(
                 name = "Text",
@@ -488,17 +490,18 @@
         val builder = LayoutInspectorTree()
         val nodes = builder.convert(androidComposeView)
         validate(nodes, builder, checkSemantics = true) {
-            node("Column", children = listOf("Text", "Row", "Row"))
+            node("Column", children = listOf("Text", "Row", "Row"), inlined = true)
             node(
                 name = "Text",
                 isRenderNode = true,
                 mergedSemantics = "[Studio]",
-                unmergedSemantics = "[Studio]"
+                unmergedSemantics = "[Studio]",
             )
             node(
                 name = "Row",
                 children = listOf("Text", "Text"),
-                mergedSemantics = "[Hello, World]"
+                mergedSemantics = "[Hello, World]",
+                inlined = true,
             )
             node("Text", isRenderNode = true, unmergedSemantics = "[Hello]")
             node("Text", isRenderNode = true, unmergedSemantics = "[World]")
@@ -506,7 +509,8 @@
                 name = "Row",
                 children = listOf("Text", "Text"),
                 mergedSemantics = "[to]",
-                unmergedSemantics = "[to]"
+                unmergedSemantics = "[to]",
+                inlined = true,
             )
             node("Text", isRenderNode = true, unmergedSemantics = "[Hello]")
             node("Text", isRenderNode = true, unmergedSemantics = "[World]")
@@ -550,7 +554,8 @@
             node(
                 name = "Column",
                 fileName = "LayoutInspectorTreeTest.kt",
-                children = listOf("Text")
+                children = listOf("Text"),
+                inlined = true,
             )
             node(
                 name = "Text",
@@ -614,7 +619,8 @@
                 name = "Column",
                 isRenderNode = true,
                 fileName = "LayoutInspectorTreeTest.kt",
-                children = listOf("Text")
+                children = listOf("Text"),
+                inlined = true,
             )
             node(
                 name = "Text",
@@ -677,6 +683,7 @@
                 name = "ComposeNode",
                 fileName = "AndroidView.android.kt",
                 hasViewIdUnder = composeView,
+                inlined = true,
             )
         }
     }
@@ -785,12 +792,13 @@
         dumpNodes(nodes, androidComposeView, builder)
 
         validate(nodes, builder, checkLineNumbers = true, checkRenderNodes = false) {
-            node("Column", lineNumber = testLine + 5, children = listOf("Title"))
+            node("Column", lineNumber = testLine + 5, children = listOf("Title"), inlined = true)
             node("Title", lineNumber = testLine + 6, children = listOf("Column"))
             node(
                 name = "Column",
                 lineNumber = titleLine + 4,
-                children = listOf("Spacer", "Text", "Text", "Spacer", "Text", "Spacer")
+                children = listOf("Spacer", "Text", "Text", "Spacer", "Text", "Spacer"),
+                inlined = true,
             )
             node("Spacer", lineNumber = titleLine + 11)
             node("Text", lineNumber = titleLine + 12)
@@ -957,6 +965,7 @@
             fileName: String? = null,
             lineNumber: Int = -1,
             isRenderNode: Boolean = false,
+            inlined: Boolean = false,
             hasViewIdUnder: View? = null,
             hasTransformations: Boolean = false,
             mergedSemantics: String = "",
@@ -978,6 +987,7 @@
             if (lineNumber != -1) {
                 assertWithMessage(message).that(node.lineNumber).isEqualTo(lineNumber)
             }
+            assertWithMessage(message).that(node.inlined).isEqualTo(inlined)
             if (checkRenderNodes) {
                 if (isRenderNode) {
                     assertWithMessage(message).that(node.id).isGreaterThan(0L)
diff --git a/compose/ui/ui-inspection/src/main/java/androidx/compose/ui/inspection/inspector/InspectorNode.kt b/compose/ui/ui-inspection/src/main/java/androidx/compose/ui/inspection/inspector/InspectorNode.kt
index bebe4e1..4819ad7 100644
--- a/compose/ui/ui-inspection/src/main/java/androidx/compose/ui/inspection/inspector/InspectorNode.kt
+++ b/compose/ui/ui-inspection/src/main/java/androidx/compose/ui/inspection/inspector/InspectorNode.kt
@@ -96,6 +96,11 @@
     val bounds: QuadBounds? = null,
 
     /**
+     * True if the code for the Composable was inlined
+     */
+    val inlined: Boolean = false,
+
+    /**
      * The parameters of this Composable.
      */
     val parameters: List<RawParameter>,
@@ -191,6 +196,7 @@
     var length = 0
     var box: IntRect = emptyBox
     var bounds: QuadBounds? = null
+    var inlined = false
     val parameters = mutableListOf<RawParameter>()
     var viewId = UNDEFINED_ID
     val children = mutableListOf<InspectorNode>()
@@ -207,6 +213,7 @@
         unmergedSemantics.clear()
         box = emptyBox
         bounds = null
+        inlined = false
         outerBox = outsideBox
         children.clear()
     }
@@ -232,6 +239,7 @@
         length = node.length
         box = node.box
         bounds = node.bounds
+        inlined = node.inlined
         mergedSemantics.addAll(node.mergedSemantics)
         unmergedSemantics.addAll(node.unmergedSemantics)
         parameters.addAll(node.parameters)
@@ -241,7 +249,7 @@
     fun build(withSemantics: Boolean = true): InspectorNode =
         InspectorNode(
             id, key, anchorId, name, fileName, packageHash, lineNumber, offset, length,
-            box, bounds, parameters.toList(), viewId,
+            box, bounds, inlined, parameters.toList(), viewId,
             if (withSemantics) mergedSemantics.toList() else emptyList(),
             if (withSemantics) unmergedSemantics.toList() else emptyList(),
             children.toList()
diff --git a/compose/ui/ui-inspection/src/main/java/androidx/compose/ui/inspection/inspector/LayoutInspectorTree.kt b/compose/ui/ui-inspection/src/main/java/androidx/compose/ui/inspection/inspector/LayoutInspectorTree.kt
index 857b171..4c04bc6 100644
--- a/compose/ui/ui-inspection/src/main/java/androidx/compose/ui/inspection/inspector/LayoutInspectorTree.kt
+++ b/compose/ui/ui-inspection/src/main/java/androidx/compose/ui/inspection/inspector/LayoutInspectorTree.kt
@@ -452,6 +452,7 @@
         val node = newNode()
         node.name = context.name ?: ""
         node.key = group.key as? Int ?: 0
+        node.inlined = context.isInline
         val layoutInfo = group.node as? LayoutInfo
         if (layoutInfo != null) {
             return parseLayoutInfo(layoutInfo, context, node)
diff --git a/compose/ui/ui-inspection/src/main/java/androidx/compose/ui/inspection/proto/ComposeExtensions.kt b/compose/ui/ui-inspection/src/main/java/androidx/compose/ui/inspection/proto/ComposeExtensions.kt
index 5e0e39a..91fe56b 100644
--- a/compose/ui/ui-inspection/src/main/java/androidx/compose/ui/inspection/proto/ComposeExtensions.kt
+++ b/compose/ui/ui-inspection/src/main/java/androidx/compose/ui/inspection/proto/ComposeExtensions.kt
@@ -111,6 +111,9 @@
     if (unmergedSemantics.isNotEmpty()) {
         flags = flags or ComposableNode.Flags.HAS_UNMERGED_SEMANTICS_VALUE
     }
+    if (inlined) {
+        flags = flags or ComposableNode.Flags.INLINED_VALUE
+    }
     return flags
 }
 
diff --git a/compose/ui/ui-inspection/src/main/proto/compose_layout_inspection.proto b/compose/ui/ui-inspection/src/main/proto/compose_layout_inspection.proto
index c085b93..0d54474 100644
--- a/compose/ui/ui-inspection/src/main/proto/compose_layout_inspection.proto
+++ b/compose/ui/ui-inspection/src/main/proto/compose_layout_inspection.proto
@@ -88,6 +88,7 @@
       SYSTEM_CREATED = 0x1;
       HAS_MERGED_SEMANTICS = 0x2;
       HAS_UNMERGED_SEMANTICS = 0x4;
+      INLINED = 0x8;
     }
     int32 flags = 9;
 
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 68036e2..3f30abd 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
@@ -19,7 +19,7 @@
 package androidx.compose.ui.lint
 
 import androidx.compose.lint.test.Stubs
-import androidx.compose.lint.test.compiledStub
+import androidx.compose.lint.test.bytecodeStub
 import com.android.tools.lint.checks.infrastructure.LintDetectorTest
 import com.android.tools.lint.checks.infrastructure.TestMode
 import com.android.tools.lint.detector.api.Detector
@@ -43,7 +43,7 @@
     /**
      * Simplified Modifier.composed stub
      */
-    private val composedStub = compiledStub(
+    private val composedStub = bytecodeStub(
         filename = "ComposedModifier.kt",
         filepath = "androidx/compose/ui",
         checksum = 0xad91cb77,
diff --git a/compose/ui/ui-lint/src/test/java/androidx/compose/ui/lint/ModifierDeclarationDetectorTest.kt b/compose/ui/ui-lint/src/test/java/androidx/compose/ui/lint/ModifierDeclarationDetectorTest.kt
index d71a432..986370b 100644
--- a/compose/ui/ui-lint/src/test/java/androidx/compose/ui/lint/ModifierDeclarationDetectorTest.kt
+++ b/compose/ui/ui-lint/src/test/java/androidx/compose/ui/lint/ModifierDeclarationDetectorTest.kt
@@ -19,7 +19,7 @@
 package androidx.compose.ui.lint
 
 import androidx.compose.lint.test.Stubs
-import androidx.compose.lint.test.compiledStub
+import androidx.compose.lint.test.bytecodeStub
 import com.android.tools.lint.checks.infrastructure.LintDetectorTest
 import com.android.tools.lint.detector.api.Detector
 import com.android.tools.lint.detector.api.Issue
@@ -45,7 +45,7 @@
         )
 
     // Simplified Density.kt stubs
-    private val DensityStub = compiledStub(
+    private val DensityStub = bytecodeStub(
         filename = "Density.kt",
         filepath = "androidx/compose/ui/unit",
         checksum = 0xaa534a7a,
@@ -71,7 +71,7 @@
     )
 
     // Simplified ParentDataModifier.kt / Measurable.kt merged stubs
-    private val MeasurableAndParentDataModifierStub = compiledStub(
+    private val MeasurableAndParentDataModifierStub = bytecodeStub(
         filename = "Measurable.kt",
         filepath = "androidx/compose/ui/layout",
         checksum = 0xd1bf915a,
@@ -769,7 +769,7 @@
 
     @Test
     fun noErrors_inlineAndValueClasses() {
-        val inlineAndValueClassStub = compiledStub(
+        val inlineAndValueClassStub = bytecodeStub(
             filename = "InlineAndValueClassStub.kt",
             filepath = "androidx/compose/ui/foo",
             checksum = 0x16c1f1c4,
diff --git a/compose/ui/ui-lint/src/test/java/androidx/compose/ui/lint/MultipleAwaitPointerEventScopesDetectorTest.kt b/compose/ui/ui-lint/src/test/java/androidx/compose/ui/lint/MultipleAwaitPointerEventScopesDetectorTest.kt
index f677e2f..13bee92 100644
--- a/compose/ui/ui-lint/src/test/java/androidx/compose/ui/lint/MultipleAwaitPointerEventScopesDetectorTest.kt
+++ b/compose/ui/ui-lint/src/test/java/androidx/compose/ui/lint/MultipleAwaitPointerEventScopesDetectorTest.kt
@@ -17,7 +17,7 @@
 package androidx.compose.ui.lint
 
 import androidx.compose.lint.test.Stubs
-import androidx.compose.lint.test.compiledStub
+import androidx.compose.lint.test.bytecodeStub
 import com.android.tools.lint.checks.infrastructure.LintDetectorTest
 import com.android.tools.lint.checks.infrastructure.TestFile
 import com.android.tools.lint.detector.api.Detector
@@ -40,7 +40,7 @@
     override fun getIssues(): MutableList<Issue> =
         mutableListOf(MultipleAwaitPointerEventScopesDetector.MultipleAwaitPointerEventScopes)
 
-    private val ForEachGestureStub: TestFile = compiledStub(
+    private val ForEachGestureStub: TestFile = bytecodeStub(
         filename = "ForEachGesture.kt",
         filepath = "androidx/compose/foundation/gestures",
         checksum = 0xf41a4b04,
diff --git a/compose/ui/ui-lint/src/test/java/androidx/compose/ui/lint/ReturnFromAwaitPointerEventScopeDetectorTest.kt b/compose/ui/ui-lint/src/test/java/androidx/compose/ui/lint/ReturnFromAwaitPointerEventScopeDetectorTest.kt
index 55d4840..e1d4102 100644
--- a/compose/ui/ui-lint/src/test/java/androidx/compose/ui/lint/ReturnFromAwaitPointerEventScopeDetectorTest.kt
+++ b/compose/ui/ui-lint/src/test/java/androidx/compose/ui/lint/ReturnFromAwaitPointerEventScopeDetectorTest.kt
@@ -17,7 +17,7 @@
 package androidx.compose.ui.lint
 
 import androidx.compose.lint.test.Stubs
-import androidx.compose.lint.test.compiledStub
+import androidx.compose.lint.test.bytecodeStub
 import com.android.tools.lint.checks.infrastructure.LintDetectorTest
 import com.android.tools.lint.checks.infrastructure.TestFile
 import com.android.tools.lint.detector.api.Detector
@@ -33,7 +33,7 @@
     override fun getIssues(): MutableList<Issue> =
         mutableListOf(ReturnFromAwaitPointerEventScopeDetector.ExitAwaitPointerEventScope)
 
-    private val ForEachGestureStub: TestFile = compiledStub(
+    private val ForEachGestureStub: TestFile = bytecodeStub(
         filename = "ForEachGesture.kt",
         filepath = "androidx/compose/foundation/gestures",
         checksum = 0xf41a4b04,
diff --git a/compose/ui/ui-lint/src/test/java/androidx/compose/ui/lint/UiStubs.kt b/compose/ui/ui-lint/src/test/java/androidx/compose/ui/lint/UiStubs.kt
index 4530026..e32d2e1 100644
--- a/compose/ui/ui-lint/src/test/java/androidx/compose/ui/lint/UiStubs.kt
+++ b/compose/ui/ui-lint/src/test/java/androidx/compose/ui/lint/UiStubs.kt
@@ -16,12 +16,12 @@
 
 package androidx.compose.ui.lint
 
-import androidx.compose.lint.test.compiledStub
+import androidx.compose.lint.test.bytecodeStub
 import com.android.tools.lint.checks.infrastructure.TestFile
 
 object UiStubs {
 
-    val Density: TestFile = compiledStub(
+    val Density: TestFile = bytecodeStub(
         filename = "Density.kt",
         filepath = "androidx/compose/ui/unit",
         checksum = 0x8c5922ca,
@@ -48,7 +48,7 @@
                 """
     )
 
-    val PointerEvent: TestFile = compiledStub(
+    val PointerEvent: TestFile = bytecodeStub(
         filename = "PointerEvent.kt",
         filepath = "androidx/compose/ui/input/pointer",
         checksum = 0xbe2705da,
@@ -115,7 +115,7 @@
                 """
     )
 
-    val PointerInputScope: TestFile = compiledStub(
+    val PointerInputScope: TestFile = bytecodeStub(
         filename = "SuspendingPointerInputFilter.kt",
         filepath = "androidx/compose/ui/input/pointer",
         checksum = 0xd7db138c,
@@ -184,7 +184,7 @@
                 """
     )
 
-    val Alignment: TestFile = compiledStub(
+    val Alignment: TestFile = bytecodeStub(
         filename = "Alignment.kt",
         filepath = "androidx/compose/ui",
         checksum = 0xd737b17c,
diff --git a/compose/ui/ui-test-manifest-lint/src/main/java/androidx/compose/ui/test/manifest/lint/GradleDebugConfigurationDetector.kt b/compose/ui/ui-test-manifest-lint/src/main/java/androidx/compose/ui/test/manifest/lint/GradleDebugConfigurationDetector.kt
index 3b16ca6..f5ed65b 100644
--- a/compose/ui/ui-test-manifest-lint/src/main/java/androidx/compose/ui/test/manifest/lint/GradleDebugConfigurationDetector.kt
+++ b/compose/ui/ui-test-manifest-lint/src/main/java/androidx/compose/ui/test/manifest/lint/GradleDebugConfigurationDetector.kt
@@ -21,6 +21,7 @@
 import com.android.tools.lint.detector.api.GradleContext
 import com.android.tools.lint.detector.api.GradleScanner
 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.Scope
 import com.android.tools.lint.detector.api.Severity
@@ -90,14 +91,17 @@
                     it
                 )
             }) {
-            context.report(
-                ISSUE, statementCookie, context.getLocation(statementCookie),
-                "Please use debugImplementation.",
-                fix().replace()
-                    .text(property)
-                    .with("debugImplementation")
-                    .build()
-            )
+            val incident = Incident(context)
+                    .issue(ISSUE)
+                    .location(context.getLocation(statementCookie))
+                    .message("Please use debugImplementation.")
+                    .fix(
+                        fix().replace()
+                            .text(property)
+                            .with("debugImplementation")
+                            .build()
+                    )
+            context.report(incident)
         }
     }
 
diff --git a/compose/ui/ui-tooling/build.gradle b/compose/ui/ui-tooling/build.gradle
index 0cae782..04f8fc0 100644
--- a/compose/ui/ui-tooling/build.gradle
+++ b/compose/ui/ui-tooling/build.gradle
@@ -38,6 +38,7 @@
         api(project(":compose:ui:ui"))
         api(project(":compose:ui:ui-tooling-preview"))
         api(project(":compose:ui:ui-tooling-data"))
+        implementation(project(":lifecycle:lifecycle-viewmodel"))
         implementation("androidx.savedstate:savedstate-ktx:1.2.0")
         implementation("androidx.compose.material:material:1.0.0")
         implementation("androidx.activity:activity-compose:1.3.0")
diff --git a/compose/ui/ui-tooling/src/androidMain/kotlin/androidx/compose/ui/tooling/ComposeViewAdapter.kt b/compose/ui/ui-tooling/src/androidMain/kotlin/androidx/compose/ui/tooling/ComposeViewAdapter.kt
index df601bb..62ec3a6 100644
--- a/compose/ui/ui-tooling/src/androidMain/kotlin/androidx/compose/ui/tooling/ComposeViewAdapter.kt
+++ b/compose/ui/ui-tooling/src/androidMain/kotlin/androidx/compose/ui/tooling/ComposeViewAdapter.kt
@@ -65,7 +65,7 @@
 import androidx.lifecycle.ViewModelStore
 import androidx.lifecycle.ViewModelStoreOwner
 import androidx.lifecycle.setViewTreeLifecycleOwner
-import androidx.lifecycle.ViewTreeViewModelStoreOwner
+import androidx.lifecycle.setViewTreeViewModelStoreOwner
 import androidx.savedstate.SavedStateRegistry
 import androidx.savedstate.SavedStateRegistryController
 import androidx.savedstate.SavedStateRegistryOwner
@@ -575,7 +575,7 @@
         // ComposeView and lifecycle initialization
         setViewTreeLifecycleOwner(FakeSavedStateRegistryOwner)
         setViewTreeSavedStateRegistryOwner(FakeSavedStateRegistryOwner)
-        ViewTreeViewModelStoreOwner.set(this, FakeViewModelStoreOwner)
+        setViewTreeViewModelStoreOwner(FakeViewModelStoreOwner)
         addView(composeView)
 
         val composableName = attrs.getAttributeValue(TOOLS_NS_URI, "composableName") ?: return
diff --git a/compose/ui/ui-unit/api/current.txt b/compose/ui/ui-unit/api/current.txt
index 353e98d..20e0534 100644
--- a/compose/ui/ui-unit/api/current.txt
+++ b/compose/ui/ui-unit/api/current.txt
@@ -292,6 +292,8 @@
     method @androidx.compose.runtime.Stable public static androidx.compose.ui.unit.IntRect IntRect(long topLeft, long bottomRight);
     method @androidx.compose.runtime.Stable public static androidx.compose.ui.unit.IntRect IntRect(long center, int radius);
     method @androidx.compose.runtime.Stable public static androidx.compose.ui.unit.IntRect lerp(androidx.compose.ui.unit.IntRect start, androidx.compose.ui.unit.IntRect stop, float fraction);
+    method @androidx.compose.runtime.Stable public static androidx.compose.ui.unit.IntRect round(androidx.compose.ui.geometry.Rect);
+    method @androidx.compose.runtime.Stable public static androidx.compose.ui.geometry.Rect toRect(androidx.compose.ui.unit.IntRect);
   }
 
   @androidx.compose.runtime.Immutable @kotlin.jvm.JvmInline public final value class IntSize {
diff --git a/compose/ui/ui-unit/api/public_plus_experimental_current.txt b/compose/ui/ui-unit/api/public_plus_experimental_current.txt
index 941576b..ea478c4 100644
--- a/compose/ui/ui-unit/api/public_plus_experimental_current.txt
+++ b/compose/ui/ui-unit/api/public_plus_experimental_current.txt
@@ -295,6 +295,8 @@
     method @androidx.compose.runtime.Stable public static androidx.compose.ui.unit.IntRect IntRect(long topLeft, long bottomRight);
     method @androidx.compose.runtime.Stable public static androidx.compose.ui.unit.IntRect IntRect(long center, int radius);
     method @androidx.compose.runtime.Stable public static androidx.compose.ui.unit.IntRect lerp(androidx.compose.ui.unit.IntRect start, androidx.compose.ui.unit.IntRect stop, float fraction);
+    method @androidx.compose.runtime.Stable public static androidx.compose.ui.unit.IntRect round(androidx.compose.ui.geometry.Rect);
+    method @androidx.compose.runtime.Stable public static androidx.compose.ui.geometry.Rect toRect(androidx.compose.ui.unit.IntRect);
   }
 
   @androidx.compose.runtime.Immutable @kotlin.jvm.JvmInline public final value class IntSize {
diff --git a/compose/ui/ui-unit/api/restricted_current.txt b/compose/ui/ui-unit/api/restricted_current.txt
index 0817101..36d5a96 100644
--- a/compose/ui/ui-unit/api/restricted_current.txt
+++ b/compose/ui/ui-unit/api/restricted_current.txt
@@ -292,6 +292,8 @@
     method @androidx.compose.runtime.Stable public static androidx.compose.ui.unit.IntRect IntRect(long topLeft, long bottomRight);
     method @androidx.compose.runtime.Stable public static androidx.compose.ui.unit.IntRect IntRect(long center, int radius);
     method @androidx.compose.runtime.Stable public static androidx.compose.ui.unit.IntRect lerp(androidx.compose.ui.unit.IntRect start, androidx.compose.ui.unit.IntRect stop, float fraction);
+    method @androidx.compose.runtime.Stable public static androidx.compose.ui.unit.IntRect round(androidx.compose.ui.geometry.Rect);
+    method @androidx.compose.runtime.Stable public static androidx.compose.ui.geometry.Rect toRect(androidx.compose.ui.unit.IntRect);
   }
 
   @androidx.compose.runtime.Immutable @kotlin.jvm.JvmInline public final value class IntSize {
diff --git a/compose/ui/ui-unit/src/commonMain/kotlin/androidx/compose/ui/unit/IntRect.kt b/compose/ui/ui-unit/src/commonMain/kotlin/androidx/compose/ui/unit/IntRect.kt
index b7b5c9d..4db5801 100644
--- a/compose/ui/ui-unit/src/commonMain/kotlin/androidx/compose/ui/unit/IntRect.kt
+++ b/compose/ui/ui-unit/src/commonMain/kotlin/androidx/compose/ui/unit/IntRect.kt
@@ -21,9 +21,11 @@
 import androidx.compose.runtime.Immutable
 import androidx.compose.runtime.Stable
 import androidx.compose.ui.geometry.Offset
+import androidx.compose.ui.geometry.Rect
 import androidx.compose.ui.geometry.translate
 import androidx.compose.ui.util.lerp
 import kotlin.math.absoluteValue
+import kotlin.math.roundToInt
 
 /**
  * An immutable, 2D, axis-aligned, integer bounds rectangle whose coordinates are relative
@@ -308,3 +310,25 @@
         lerp(start.bottom, stop.bottom, fraction)
     )
 }
+
+/**
+ * Converts an [IntRect] to a [Rect]
+ */
+@Stable
+fun IntRect.toRect(): Rect = Rect(
+    left = left.toFloat(),
+    top = top.toFloat(),
+    right = right.toFloat(),
+    bottom = bottom.toFloat()
+)
+
+/**
+ * Rounds a [Rect] to an [IntRect]
+ */
+@Stable
+fun Rect.round(): IntRect = IntRect(
+    left = left.roundToInt(),
+    top = top.roundToInt(),
+    right = right.roundToInt(),
+    bottom = bottom.roundToInt()
+)
diff --git a/compose/ui/ui-unit/src/test/kotlin/androidx/compose/ui/unit/IntRectTest.kt b/compose/ui/ui-unit/src/test/kotlin/androidx/compose/ui/unit/IntRectTest.kt
index 34f10c3..463faec 100644
--- a/compose/ui/ui-unit/src/test/kotlin/androidx/compose/ui/unit/IntRectTest.kt
+++ b/compose/ui/ui-unit/src/test/kotlin/androidx/compose/ui/unit/IntRectTest.kt
@@ -16,6 +16,7 @@
 
 package androidx.compose.ui.unit
 
+import androidx.compose.ui.geometry.Rect
 import org.junit.Assert
 import org.junit.Test
 import org.junit.runner.RunWith
@@ -235,4 +236,20 @@
             lerp(rect1, rect2, 0.5f)
         )
     }
+
+    @Test
+    fun `to Rect`() {
+        Assert.assertEquals(
+            Rect(25.0f, 25.0f, 150.0f, 150.0f),
+            IntRect(25, 25, 150, 150).toRect()
+        )
+    }
+
+    @Test
+    fun `round Rect to IntRect`() {
+        Assert.assertEquals(
+            IntRect(2, 3, 4, 5),
+            Rect(2.4f, 2.5f, 3.9f, 5.3f).round(),
+        )
+    }
 }
\ No newline at end of file
diff --git a/compose/ui/ui/api/current.txt b/compose/ui/ui/api/current.txt
index 2ac063c..4eea595 100644
--- a/compose/ui/ui/api/current.txt
+++ b/compose/ui/ui/api/current.txt
@@ -2089,7 +2089,7 @@
   }
 
   public static fun interface PinnableContainer.PinnedHandle {
-    method public void unpin();
+    method public void release();
   }
 
   public final class PinnableContainerKt {
diff --git a/compose/ui/ui/api/public_plus_experimental_current.txt b/compose/ui/ui/api/public_plus_experimental_current.txt
index b6e2c40..d167cda 100644
--- a/compose/ui/ui/api/public_plus_experimental_current.txt
+++ b/compose/ui/ui/api/public_plus_experimental_current.txt
@@ -2280,7 +2280,7 @@
   }
 
   public static fun interface PinnableContainer.PinnedHandle {
-    method public void unpin();
+    method public void release();
   }
 
   public final class PinnableContainerKt {
@@ -2594,10 +2594,6 @@
 
   @androidx.compose.ui.ExperimentalComposeUiApi public interface ObserverNode extends androidx.compose.ui.node.DelegatableNode {
     method public void onObservedReadsChanged();
-    field public static final androidx.compose.ui.node.ObserverNode.Companion Companion;
-  }
-
-  @androidx.compose.ui.ExperimentalComposeUiApi public static final class ObserverNode.Companion {
   }
 
   public final class ObserverNodeKt {
diff --git a/compose/ui/ui/api/restricted_current.txt b/compose/ui/ui/api/restricted_current.txt
index 32d140b..6165576 100644
--- a/compose/ui/ui/api/restricted_current.txt
+++ b/compose/ui/ui/api/restricted_current.txt
@@ -2092,7 +2092,7 @@
   }
 
   public static fun interface PinnableContainer.PinnedHandle {
-    method public void unpin();
+    method public void release();
   }
 
   public final class PinnableContainerKt {
diff --git a/compose/ui/ui/build.gradle b/compose/ui/ui/build.gradle
index fb3978f..f9d4bb9 100644
--- a/compose/ui/ui/build.gradle
+++ b/compose/ui/ui/build.gradle
@@ -78,7 +78,7 @@
         implementation("androidx.savedstate:savedstate-ktx:1.2.0")
         implementation("androidx.lifecycle:lifecycle-common-java8:2.5.1")
         implementation(project(":lifecycle:lifecycle-runtime"))
-        implementation("androidx.lifecycle:lifecycle-viewmodel:2.5.1")
+        implementation(project(":lifecycle:lifecycle-viewmodel"))
         implementation("androidx.profileinstaller:profileinstaller:1.2.1")
         implementation("androidx.emoji2:emoji2:1.2.0")
 
@@ -175,7 +175,7 @@
                 implementation("androidx.savedstate:savedstate-ktx:1.2.0")
                 implementation("androidx.lifecycle:lifecycle-common-java8:2.5.1")
                 implementation(project(":lifecycle:lifecycle-runtime"))
-                implementation("androidx.lifecycle:lifecycle-viewmodel:2.5.1")
+                implementation(project(":lifecycle:lifecycle-viewmodel"))
                 implementation("androidx.emoji2:emoji2:1.2.0")
             }
 
diff --git a/compose/ui/ui/integration-tests/ui-demos/src/main/java/androidx/compose/ui/demos/focus/LazyListChildFocusDemos.kt b/compose/ui/ui/integration-tests/ui-demos/src/main/java/androidx/compose/ui/demos/focus/LazyListChildFocusDemos.kt
index 80c066f6..99049d9 100644
--- a/compose/ui/ui/integration-tests/ui-demos/src/main/java/androidx/compose/ui/demos/focus/LazyListChildFocusDemos.kt
+++ b/compose/ui/ui/integration-tests/ui-demos/src/main/java/androidx/compose/ui/demos/focus/LazyListChildFocusDemos.kt
@@ -110,7 +110,7 @@
                     )
                     DisposableEffect(pinnableContainer) {
                         onDispose {
-                            pinnedHandle?.unpin()
+                            pinnedHandle?.release()
                             pinnedHandle = null
                         }
                     }
diff --git a/compose/ui/ui/proguard-rules.pro b/compose/ui/ui/proguard-rules.pro
index 0e89990..1dbcbf7 100644
--- a/compose/ui/ui/proguard-rules.pro
+++ b/compose/ui/ui/proguard-rules.pro
@@ -25,3 +25,8 @@
 -keepclassmembers class androidx.compose.ui.platform.AndroidComposeView {
     android.view.View findViewByAccessibilityIdTraversal(int);
 }
+
+# Users can create Modifier.Node instances that implement multiple Modifier.Node interfaces,
+# so we cannot tell whether two modifier.node instances are of the same type without using
+# reflection to determine the class type. See b/265188224 for more context.
+-keep,allowshrinking class * extends androidx.compose.ui.node.ModifierNodeElement
diff --git a/compose/ui/ui/src/androidAndroidTest/kotlin/androidx/compose/ui/AndroidComposeViewAccessibilityDelegateCompatTest.kt b/compose/ui/ui/src/androidAndroidTest/kotlin/androidx/compose/ui/AndroidComposeViewAccessibilityDelegateCompatTest.kt
index 9e1503d..fb76f71 100644
--- a/compose/ui/ui/src/androidAndroidTest/kotlin/androidx/compose/ui/AndroidComposeViewAccessibilityDelegateCompatTest.kt
+++ b/compose/ui/ui/src/androidAndroidTest/kotlin/androidx/compose/ui/AndroidComposeViewAccessibilityDelegateCompatTest.kt
@@ -29,7 +29,6 @@
 import androidx.compose.runtime.snapshots.Snapshot
 import androidx.compose.runtime.structuralEqualityPolicy
 import androidx.compose.ui.geometry.Offset
-import androidx.compose.ui.geometry.Rect
 import androidx.compose.ui.graphics.toAndroidRect
 import androidx.compose.ui.node.InnerNodeCoordinator
 import androidx.compose.ui.node.LayoutNode
@@ -83,6 +82,7 @@
 import androidx.compose.ui.test.junit4.createAndroidComposeRule
 import androidx.compose.ui.text.AnnotatedString
 import androidx.compose.ui.text.TextRange
+import androidx.compose.ui.unit.IntRect
 import androidx.core.view.ViewCompat
 import androidx.core.view.accessibility.AccessibilityNodeInfoCompat
 import androidx.core.view.accessibility.AccessibilityNodeProviderCompat
@@ -959,7 +959,7 @@
         assertEquals(1, nodes.size)
         assertEquals(AccessibilityNodeProviderCompat.HOST_VIEW_ID, nodes.keys.first())
         assertEquals(
-            Rect.Zero.toAndroidRect(),
+            IntRect.Zero.toAndroidRect(),
             nodes[AccessibilityNodeProviderCompat.HOST_VIEW_ID]!!.adjustedBounds
         )
     }
diff --git a/compose/ui/ui/src/androidAndroidTest/kotlin/androidx/compose/ui/layout/SubcomposeLayoutTest.kt b/compose/ui/ui/src/androidAndroidTest/kotlin/androidx/compose/ui/layout/SubcomposeLayoutTest.kt
index f443ff1..45e7c80 100644
--- a/compose/ui/ui/src/androidAndroidTest/kotlin/androidx/compose/ui/layout/SubcomposeLayoutTest.kt
+++ b/compose/ui/ui/src/androidAndroidTest/kotlin/androidx/compose/ui/layout/SubcomposeLayoutTest.kt
@@ -51,7 +51,6 @@
 import androidx.compose.ui.graphics.Color
 import androidx.compose.ui.graphics.ImageBitmap
 import androidx.compose.ui.graphics.asAndroidBitmap
-import androidx.compose.ui.layout.RootMeasurePolicy.measure
 import androidx.compose.ui.platform.AndroidOwnerExtraAssertionsRule
 import androidx.compose.ui.platform.ComposeView
 import androidx.compose.ui.platform.LocalDensity
@@ -105,14 +104,20 @@
         rule.setContent {
             SubcomposeLayout { constraints ->
                 val first = subcompose(0) {
-                    Spacer(Modifier.requiredSize(50.dp).testTag(firstTag))
+                    Spacer(
+                        Modifier
+                            .requiredSize(50.dp)
+                            .testTag(firstTag))
                 }.first().measure(constraints)
 
                 // it is an input for the second subcomposition
                 val halfFirstSize = (first.width / 2).toDp()
 
                 val second = subcompose(1) {
-                    Spacer(Modifier.requiredSize(halfFirstSize).testTag(secondTag))
+                    Spacer(
+                        Modifier
+                            .requiredSize(halfFirstSize)
+                            .testTag(secondTag))
                 }.first().measure(constraints)
 
                 layout(first.width, first.height) {
@@ -142,8 +147,14 @@
         rule.setContent {
             SubcomposeLayout(Modifier.testTag(layoutTag)) { constraints ->
                 val placeables = subcompose(Unit) {
-                    Spacer(Modifier.requiredSize(50.dp).testTag(firstTag))
-                    Spacer(Modifier.requiredSize(30.dp).testTag(secondTag))
+                    Spacer(
+                        Modifier
+                            .requiredSize(50.dp)
+                            .testTag(firstTag))
+                    Spacer(
+                        Modifier
+                            .requiredSize(30.dp)
+                            .testTag(secondTag))
                 }.map {
                     it.measure(constraints)
                 }
@@ -252,7 +263,10 @@
             SubcomposeLayout(Modifier.testTag(layoutTag)) { constraints ->
                 val placeables = subcompose(Unit) {
                     if (addChild.value) {
-                        Spacer(Modifier.requiredSize(20.dp).testTag(childTag))
+                        Spacer(
+                            Modifier
+                                .requiredSize(20.dp)
+                                .testTag(childTag))
                     }
                 }.map { it.measure(constraints) }
 
@@ -297,7 +311,10 @@
 
         rule.runOnIdle {
             content.value = {
-                Spacer(Modifier.requiredSize(10.dp).testTag(updatedTag))
+                Spacer(
+                    Modifier
+                        .requiredSize(10.dp)
+                        .testTag(updatedTag))
             }
         }
 
@@ -362,10 +379,16 @@
         rule.setContent {
             SubcomposeLayout(Modifier.testTag(layoutTag)) { constraints ->
                 val first = subcompose(Color.Red) {
-                    Spacer(Modifier.requiredSize(10.dp).background(Color.Red))
+                    Spacer(
+                        Modifier
+                            .requiredSize(10.dp)
+                            .background(Color.Red))
                 }.first().measure(constraints)
                 val second = subcompose(Color.Green) {
-                    Spacer(Modifier.requiredSize(10.dp).background(Color.Green))
+                    Spacer(
+                        Modifier
+                            .requiredSize(10.dp)
+                            .background(Color.Green))
                 }.first().measure(constraints)
                 layout(first.width, first.height) {
                     first.place(0, 0)
@@ -392,10 +415,16 @@
                 val firstColor = if (firstSlotIsRed.value) Color.Red else Color.Green
                 val secondColor = if (firstSlotIsRed.value) Color.Green else Color.Red
                 val first = subcompose(firstColor) {
-                    Spacer(Modifier.requiredSize(10.dp).background(firstColor))
+                    Spacer(
+                        Modifier
+                            .requiredSize(10.dp)
+                            .background(firstColor))
                 }.first().measure(constraints)
                 val second = subcompose(secondColor) {
-                    Spacer(Modifier.requiredSize(10.dp).background(secondColor))
+                    Spacer(
+                        Modifier
+                            .requiredSize(10.dp)
+                            .background(secondColor))
                 }.first().measure(constraints)
                 layout(first.width, first.height) {
                     first.place(0, 0)
@@ -425,10 +454,17 @@
         rule.setContent {
             SubcomposeLayout(Modifier.testTag(layoutTag)) { constraints ->
                 val first = subcompose(Color.Red) {
-                    Spacer(Modifier.requiredSize(10.dp).background(Color.Red).zIndex(1f))
+                    Spacer(
+                        Modifier
+                            .requiredSize(10.dp)
+                            .background(Color.Red)
+                            .zIndex(1f))
                 }.first().measure(constraints)
                 val second = subcompose(Color.Green) {
-                    Spacer(Modifier.requiredSize(10.dp).background(Color.Green))
+                    Spacer(
+                        Modifier
+                            .requiredSize(10.dp)
+                            .background(Color.Green))
                 }.first().measure(constraints)
                 layout(first.width, first.height) {
                     first.place(0, 0)
@@ -491,9 +527,11 @@
             val sizeIpx = with(density) { size.roundToPx() }
             CompositionLocalProvider(LocalDensity provides density) {
                 SubcomposeLayout(
-                    Modifier.requiredSize(size).onGloballyPositioned {
-                        assertThat(it.size).isEqualTo(IntSize(sizeIpx, sizeIpx))
-                    }
+                    Modifier
+                        .requiredSize(size)
+                        .onGloballyPositioned {
+                            assertThat(it.size).isEqualTo(IntSize(sizeIpx, sizeIpx))
+                        }
                 ) { constraints ->
                     layout(constraints.maxWidth, constraints.maxHeight) {}
                 }
@@ -510,10 +548,16 @@
         rule.setContent {
             SubcomposeLayout(Modifier.testTag(layoutTag)) { constraints ->
                 val first = subcompose(Color.Red) {
-                    Spacer(Modifier.requiredSize(10.dp).background(Color.Red))
+                    Spacer(
+                        Modifier
+                            .requiredSize(10.dp)
+                            .background(Color.Red))
                 }.first().measure(constraints)
                 val second = subcompose(Color.Green) {
-                    Spacer(Modifier.requiredSize(10.dp).background(Color.Green))
+                    Spacer(
+                        Modifier
+                            .requiredSize(10.dp)
+                            .background(Color.Green))
                 }.first().measure(constraints)
 
                 layout(first.width, first.height) {
@@ -1300,7 +1344,10 @@
 
             SubcomposeLayout(state = state) {
                 val placeable = subcompose(Unit) {
-                    Box(Modifier.size(10.dp).testTag(tag))
+                    Box(
+                        Modifier
+                            .size(10.dp)
+                            .testTag(tag))
                 }.first().measure(Constraints())
                 layout(placeable.width, placeable.height) {
                     placeable.place(0, 0)
@@ -1736,14 +1783,16 @@
         var remeasuresCount = 0
         var relayoutCount = 0
         var subcomposeLayoutRemeasures = 0
-        val modifier = Modifier.layout { measurable, constraints ->
-            val placeable = measurable.measure(constraints)
-            remeasuresCount++
-            layout(placeable.width, placeable.height) {
-                relayoutCount++
-                placeable.place(0, 0)
+        val modifier = Modifier
+            .layout { measurable, constraints ->
+                val placeable = measurable.measure(constraints)
+                remeasuresCount++
+                layout(placeable.width, placeable.height) {
+                    relayoutCount++
+                    placeable.place(0, 0)
+                }
             }
-        }.fillMaxSize()
+            .fillMaxSize()
         val content = @Composable { Box(modifier) }
         val constraints = Constraints(maxWidth = 100, minWidth = 100)
         var needContent by mutableStateOf(false)
@@ -1789,13 +1838,15 @@
     fun premeasuringTwoPlaceables() {
         val state = SubcomposeLayoutState()
         var remeasuresCount = 0
-        val modifier = Modifier.layout { measurable, constraints ->
-            val placeable = measurable.measure(constraints)
-            remeasuresCount++
-            layout(placeable.width, placeable.height) {
-                placeable.place(0, 0)
+        val modifier = Modifier
+            .layout { measurable, constraints ->
+                val placeable = measurable.measure(constraints)
+                remeasuresCount++
+                layout(placeable.width, placeable.height) {
+                    placeable.place(0, 0)
+                }
             }
-        }.fillMaxSize()
+            .fillMaxSize()
         val content = @Composable {
             Box(modifier)
             Box(modifier)
@@ -2041,7 +2092,9 @@
 
         val handle = rule.runOnIdle {
             state.precompose(1) {
-                Box(modifier = Modifier.size(10.dp).testTag("1"))
+                Box(modifier = Modifier
+                    .size(10.dp)
+                    .testTag("1"))
             }
         }
 
@@ -2248,6 +2301,23 @@
         rule.waitUntil { isActive }
     }
 
+    @Test
+    fun composingTheSameKeyTwiceIsNotAllowed() {
+        var error: Exception? = null
+        rule.setContent {
+            SubcomposeLayout { _ ->
+                subcompose(0) {}
+                try {
+                    subcompose(0) {}
+                } catch (e: Exception) {
+                    error = e
+                }
+                layout(100, 100) {}
+            }
+        }
+        assertThat(error).isInstanceOf(IllegalArgumentException::class.java)
+    }
+
     private fun composeItems(
         state: SubcomposeLayoutState,
         items: MutableState<List<Int>>
@@ -2268,7 +2338,10 @@
 
     @Composable
     private fun ItemContent(index: Int) {
-        Box(Modifier.fillMaxSize().testTag("$index"))
+        Box(
+            Modifier
+                .fillMaxSize()
+                .testTag("$index"))
     }
 
     private fun assertNodes(exists: List<Int>, doesNotExist: List<Int> = emptyList()) {
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 db2474f..f2876ab 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
@@ -2279,7 +2279,7 @@
     }
 
     private fun sendScrollEventIfNeeded(scrollObservationScope: ScrollObservationScope) {
-        if (!scrollObservationScope.isValid) {
+        if (!scrollObservationScope.isValidOwnerScope) {
             return
         }
         view.snapshotObserver.observeReads(scrollObservationScope, sendScrollEventIfNeededLambda) {
@@ -2879,7 +2879,7 @@
     var horizontalScrollAxisRange: ScrollAxisRange?,
     var verticalScrollAxisRange: ScrollAxisRange?
 ) : OwnerScope {
-    override val isValid get() = allScopes.contains(this)
+    override val isValidOwnerScope get() = allScopes.contains(this)
 }
 
 internal fun List<ScrollObservationScope>.findById(id: Int): ScrollObservationScope? {
diff --git a/compose/ui/ui/src/androidMain/kotlin/androidx/compose/ui/window/AndroidDialog.android.kt b/compose/ui/ui/src/androidMain/kotlin/androidx/compose/ui/window/AndroidDialog.android.kt
index 41b3bff..346e6f3 100644
--- a/compose/ui/ui/src/androidMain/kotlin/androidx/compose/ui/window/AndroidDialog.android.kt
+++ b/compose/ui/ui/src/androidMain/kotlin/androidx/compose/ui/window/AndroidDialog.android.kt
@@ -58,9 +58,10 @@
 import androidx.compose.ui.util.fastMap
 import androidx.compose.ui.util.fastMaxBy
 import androidx.core.view.WindowCompat
-import androidx.lifecycle.ViewTreeViewModelStoreOwner
 import androidx.lifecycle.findViewTreeLifecycleOwner
+import androidx.lifecycle.findViewTreeViewModelStoreOwner
 import androidx.lifecycle.setViewTreeLifecycleOwner
+import androidx.lifecycle.setViewTreeViewModelStoreOwner
 import androidx.savedstate.findViewTreeSavedStateRegistryOwner
 import androidx.savedstate.setViewTreeSavedStateRegistryOwner
 import java.util.UUID
@@ -349,7 +350,7 @@
         (window.decorView as? ViewGroup)?.disableClipping()
         setContentView(dialogLayout)
         dialogLayout.setViewTreeLifecycleOwner(composeView.findViewTreeLifecycleOwner())
-        ViewTreeViewModelStoreOwner.set(dialogLayout, ViewTreeViewModelStoreOwner.get(composeView))
+        dialogLayout.setViewTreeViewModelStoreOwner(composeView.findViewTreeViewModelStoreOwner())
         dialogLayout.setViewTreeSavedStateRegistryOwner(
             composeView.findViewTreeSavedStateRegistryOwner()
         )
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 a4c829a..dd02191 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
@@ -76,7 +76,8 @@
 import androidx.compose.ui.util.fastMap
 import androidx.lifecycle.findViewTreeLifecycleOwner
 import androidx.lifecycle.setViewTreeLifecycleOwner
-import androidx.lifecycle.ViewTreeViewModelStoreOwner
+import androidx.lifecycle.findViewTreeViewModelStoreOwner
+import androidx.lifecycle.setViewTreeViewModelStoreOwner
 import androidx.savedstate.findViewTreeSavedStateRegistryOwner
 import androidx.savedstate.setViewTreeSavedStateRegistryOwner
 import java.util.UUID
@@ -419,7 +420,7 @@
     init {
         id = android.R.id.content
         setViewTreeLifecycleOwner(composeView.findViewTreeLifecycleOwner())
-        ViewTreeViewModelStoreOwner.set(this, ViewTreeViewModelStoreOwner.get(composeView))
+        setViewTreeViewModelStoreOwner(composeView.findViewTreeViewModelStoreOwner())
         setViewTreeSavedStateRegistryOwner(composeView.findViewTreeSavedStateRegistryOwner())
         // Set unique id for AbstractComposeView. This allows state restoration for the state
         // defined inside the Popup via rememberSaveable()
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 2c43580..0d873b0 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
@@ -19,9 +19,9 @@
 import androidx.compose.runtime.Stable
 import androidx.compose.ui.internal.JvmDefaultWithCompatibility
 import androidx.compose.ui.node.DelegatableNode
+import androidx.compose.ui.node.ModifierNodeOwnerScope
 import androidx.compose.ui.node.NodeCoordinator
 import androidx.compose.ui.node.NodeKind
-import androidx.compose.ui.node.OwnerScope
 import androidx.compose.ui.node.requireOwner
 
 /**
@@ -161,7 +161,7 @@
      * @see androidx.compose.ui.node.IntermediateLayoutModifierNode
      */
     @ExperimentalComposeUiApi
-    abstract class Node : DelegatableNode, OwnerScope {
+    abstract class Node : DelegatableNode {
         @Suppress("LeakingThis")
         final override var node: Node = this
             private set
@@ -172,6 +172,7 @@
         internal var aggregateChildKindSet: Int = 0
         internal var parent: Node? = null
         internal var child: Node? = null
+        internal var ownerScope: ModifierNodeOwnerScope? = null
         internal var coordinator: NodeCoordinator? = null
             private set
         /**
@@ -186,13 +187,6 @@
         var isAttached: Boolean = false
             private set
 
-        @Deprecated(
-            message = "isValid is hidden so that we can keep the OwnerScope interface internal.",
-            level = DeprecationLevel.HIDDEN
-        )
-        override val isValid: Boolean
-            get() = isAttached
-
         internal open fun updateCoordinator(coordinator: NodeCoordinator?) {
             this.coordinator = coordinator
         }
diff --git a/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/layout/PinnableContainer.kt b/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/layout/PinnableContainer.kt
index 593e9e7..4874d63 100644
--- a/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/layout/PinnableContainer.kt
+++ b/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/layout/PinnableContainer.kt
@@ -20,35 +20,47 @@
 import androidx.compose.runtime.compositionLocalOf
 
 /**
- * Parent layouts with pinning support for the children content will provide the current
- * [PinnableContainer] via this composition local.
+ * Use this composition local to get the [PinnableContainer] handling the current subhierarchy.
+ *
+ * It will be not null, for example, when the current content is composed as an item of lazy list.
  */
 val LocalPinnableContainer = compositionLocalOf<PinnableContainer?> { null }
 
 /**
- * Represents a container which can be pinned by some of its parents.
+ * Represents a container which can be pinned when the content of this container is important.
  *
- * For example in lists which only compose visible items it means this item will be kept
- * composed even when it will be scrolled out of the view.
+ * For example, each item of lazy list represents one [PinnableContainer], and if this
+ * container is pinned, this item will not be disposed when scrolled out of the viewport.
+ *
+ * Pinning a currently focused item so the focus is not lost is one of the examples when this
+ * functionality can be useful.
+ *
+ * @see LocalPinnableContainer
  */
 @Stable
 interface PinnableContainer {
 
     /**
-     * Pin the current container when its content needs to be kept alive, for example when it has
-     * focus or user is interacting with it in some other way.
+     * Allows to pin this container when the associated content is considered important.
      *
-     * Don't forget to call [PinnedHandle.unpin] when this content is not needed anymore.
+     * For example, if this [PinnableContainer] is an item of lazy list pinning will mean
+     * this item will not be disposed when scrolled out of the viewport.
+     *
+     * Don't forget to call [PinnedHandle.release] when this content is not important anymore.
      */
     fun pin(): PinnedHandle
 
     /**
-     * This is an object returned by [pin] which allows to unpin the content.
+     * This is an object returned by [pin] which allows to release the pinning.
      */
+    @Suppress("NotCloseable")
     fun interface PinnedHandle {
         /**
-         *  Unpin the container associated with this handle.
+         * Releases the pin.
+         *
+         * For example, if this [PinnableContainer] is an item of lazy list releasing the
+         * pinning will allow lazy list to stop composing the item when it is not visible.
          */
-        fun unpin()
+        fun release()
     }
 }
diff --git a/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/layout/SubcomposeLayout.kt b/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/layout/SubcomposeLayout.kt
index 575a9ba..e6089667 100644
--- a/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/layout/SubcomposeLayout.kt
+++ b/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/layout/SubcomposeLayout.kt
@@ -407,11 +407,9 @@
         }
 
         val itemIndex = root.foldedChildren.indexOf(node)
-        if (itemIndex < currentIndex) {
-            throw IllegalArgumentException(
-                "Key $slotId was already used. If you are using LazyColumn/Row please make sure " +
-                    "you provide a unique key for each item."
-            )
+        require(itemIndex >= currentIndex) {
+            "Key \"$slotId\" was already used. If you are using LazyColumn/Row please make " +
+                "sure you provide a unique key for each item."
         }
         if (currentIndex != itemIndex) {
             move(itemIndex, currentIndex)
diff --git a/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/node/BackwardsCompatNode.kt b/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/node/BackwardsCompatNode.kt
index a415367..af4d5d2 100644
--- a/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/node/BackwardsCompatNode.kt
+++ b/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/node/BackwardsCompatNode.kt
@@ -304,7 +304,7 @@
         }
     }
 
-    override val isValid: Boolean get() = isAttached
+    override val isValidOwnerScope: Boolean get() = isAttached
     override var targetSize: IntSize
         get() = (element as IntermediateLayoutModifier).targetSize
         set(value) {
diff --git a/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/node/LayoutNode.kt b/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/node/LayoutNode.kt
index 57ee75d..a410193 100644
--- a/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/node/LayoutNode.kt
+++ b/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/node/LayoutNode.kt
@@ -466,7 +466,7 @@
             return _zSortedChildren
         }
 
-    override val isValid: Boolean
+    override val isValidOwnerScope: Boolean
         get() = isAttached
 
     override fun toString(): String {
diff --git a/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/node/NodeCoordinator.kt b/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/node/NodeCoordinator.kt
index 6de865c..01cc2c9 100644
--- a/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/node/NodeCoordinator.kt
+++ b/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/node/NodeCoordinator.kt
@@ -507,7 +507,7 @@
     var layer: OwnedLayer? = null
         private set
 
-    override val isValid: Boolean
+    override val isValidOwnerScope: Boolean
         get() = layer != null && isAttached
 
     val minimumTouchTargetSize: Size
@@ -1208,7 +1208,7 @@
             "when isAttached is true"
         const val UnmeasuredError = "Asking for measurement result of unmeasured layout modifier"
         private val onCommitAffectingLayerParams: (NodeCoordinator) -> Unit = { coordinator ->
-            if (coordinator.isValid) {
+            if (coordinator.isValidOwnerScope) {
                 // coordinator.layerPositionalProperties should always be non-null here, but
                 // we'll just be careful with a null check.
                 val layerPositionalProperties = coordinator.layerPositionalProperties
diff --git a/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/node/ObserverNode.kt b/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/node/ObserverNode.kt
index 2450ada..9d884bc 100644
--- a/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/node/ObserverNode.kt
+++ b/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/node/ObserverNode.kt
@@ -32,11 +32,16 @@
      * changes.
      */
     fun onObservedReadsChanged()
+}
 
-    @ExperimentalComposeUiApi
+@OptIn(ExperimentalComposeUiApi::class)
+internal class ModifierNodeOwnerScope(internal val observerNode: ObserverNode) : OwnerScope {
+    override val isValidOwnerScope: Boolean
+        get() = observerNode.node.isAttached
+
     companion object {
-        internal val OnObserveReadsChanged: (ObserverNode) -> Unit = {
-            if (it.node.isAttached) it.onObservedReadsChanged()
+        internal val OnObserveReadsChanged: (ModifierNodeOwnerScope) -> Unit = {
+            if (it.isValidOwnerScope) it.observerNode.onObservedReadsChanged()
         }
     }
 }
@@ -46,9 +51,10 @@
  */
 @ExperimentalComposeUiApi
 fun <T> T.observeReads(block: () -> Unit) where T : Modifier.Node, T : ObserverNode {
+    val target = ownerScope ?: ModifierNodeOwnerScope(this).also { ownerScope = it }
     requireOwner().snapshotObserver.observeReads(
-        target = this,
-        onChanged = ObserverNode.OnObserveReadsChanged,
+        target = target,
+        onChanged = ModifierNodeOwnerScope.OnObserveReadsChanged,
         block = block
     )
 }
diff --git a/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/node/OwnerScope.kt b/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/node/OwnerScope.kt
index 7c375b7..938d179 100644
--- a/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/node/OwnerScope.kt
+++ b/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/node/OwnerScope.kt
@@ -20,12 +20,12 @@
  * Read observation scopes used in layout and drawing must implement this interface to let the
  * snapshot observer know when the scope has been removed and should no longer be observed.
  *
- * @see Owner.observeReads
+ * @see OwnerSnapshotObserver.observeReads
  */
 internal interface OwnerScope {
     /**
      * `true` when the scope is still in the hierarchy and `false` once it has been removed and
      * observations are no longer necessary.
      */
-    val isValid: Boolean
+    val isValidOwnerScope: Boolean
 }
\ No newline at end of file
diff --git a/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/node/OwnerSnapshotObserver.kt b/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/node/OwnerSnapshotObserver.kt
index 4538fd4..1c6e779 100644
--- a/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/node/OwnerSnapshotObserver.kt
+++ b/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/node/OwnerSnapshotObserver.kt
@@ -28,37 +28,37 @@
     private val observer = SnapshotStateObserver(onChangedExecutor)
 
     private val onCommitAffectingLookaheadMeasure: (LayoutNode) -> Unit = { layoutNode ->
-        if (layoutNode.isValid) {
+        if (layoutNode.isValidOwnerScope) {
             layoutNode.requestLookaheadRemeasure()
         }
     }
 
     private val onCommitAffectingMeasure: (LayoutNode) -> Unit = { layoutNode ->
-        if (layoutNode.isValid) {
+        if (layoutNode.isValidOwnerScope) {
             layoutNode.requestRemeasure()
         }
     }
 
     private val onCommitAffectingLayout: (LayoutNode) -> Unit = { layoutNode ->
-        if (layoutNode.isValid) {
+        if (layoutNode.isValidOwnerScope) {
             layoutNode.requestRelayout()
         }
     }
 
     private val onCommitAffectingLayoutModifier: (LayoutNode) -> Unit = { layoutNode ->
-        if (layoutNode.isValid) {
+        if (layoutNode.isValidOwnerScope) {
             layoutNode.requestRelayout()
         }
     }
 
     private val onCommitAffectingLayoutModifierInLookahead: (LayoutNode) -> Unit = { layoutNode ->
-        if (layoutNode.isValid) {
+        if (layoutNode.isValidOwnerScope) {
             layoutNode.requestLookaheadRelayout()
         }
     }
 
     private val onCommitAffectingLookaheadLayout: (LayoutNode) -> Unit = { layoutNode ->
-        if (layoutNode.isValid) {
+        if (layoutNode.isValidOwnerScope) {
             layoutNode.requestLookaheadRelayout()
         }
     }
@@ -121,7 +121,7 @@
     }
 
     internal fun clearInvalidObservations() {
-        observer.clearIf { !(it as OwnerScope).isValid }
+        observer.clearIf { !(it as OwnerScope).isValidOwnerScope }
     }
 
     internal fun clear(target: Any) {
diff --git a/constraintlayout/constraintlayout-compose-lint/src/test/java/androidx/constraintlayout/compose/lint/ConstraintLayoutDslDetectorTest.kt b/constraintlayout/constraintlayout-compose-lint/src/test/java/androidx/constraintlayout/compose/lint/ConstraintLayoutDslDetectorTest.kt
index f8befa1..359189a 100644
--- a/constraintlayout/constraintlayout-compose-lint/src/test/java/androidx/constraintlayout/compose/lint/ConstraintLayoutDslDetectorTest.kt
+++ b/constraintlayout/constraintlayout-compose-lint/src/test/java/androidx/constraintlayout/compose/lint/ConstraintLayoutDslDetectorTest.kt
@@ -16,7 +16,7 @@
 
 package androidx.constraintlayout.compose.lint
 
-import androidx.compose.lint.test.compiledStub
+import androidx.compose.lint.test.bytecodeStub
 import com.android.tools.lint.checks.infrastructure.LintDetectorTest
 import com.android.tools.lint.detector.api.Issue
 import org.junit.Test
@@ -32,7 +32,7 @@
     override fun getIssues(): MutableList<Issue> =
         mutableListOf(ConstraintLayoutDslDetector.IncorrectReferencesDeclarationIssue)
 
-    private val ConstraintSetScopeStub = compiledStub(
+    private val ConstraintSetScopeStub = bytecodeStub(
         filename = "ConstraintSetScope.kt",
         filepath = COMPOSE_CONSTRAINTLAYOUT_FILE_PATH,
         checksum = 0x912b8878,
@@ -174,7 +174,7 @@
                 """
     )
 
-    private val MotionSceneScopeStub = compiledStub(
+    private val MotionSceneScopeStub = bytecodeStub(
         filename = "MotionSceneScope.kt",
         filepath = COMPOSE_CONSTRAINTLAYOUT_FILE_PATH,
         checksum = 0xc89561d0,
diff --git a/constraintlayout/constraintlayout-compose/api/current.txt b/constraintlayout/constraintlayout-compose/api/current.txt
index 0182e04..f80b976 100644
--- a/constraintlayout/constraintlayout-compose/api/current.txt
+++ b/constraintlayout/constraintlayout-compose/api/current.txt
@@ -411,9 +411,6 @@
   public final class MotionDragHandlerKt {
   }
 
-  @kotlin.DslMarker public @interface MotionDslScope {
-  }
-
   public interface MotionItemsProvider {
     method public int count();
     method public kotlin.jvm.functions.Function0<kotlin.Unit> getContent(int index);
diff --git a/constraintlayout/constraintlayout-compose/api/public_plus_experimental_current.txt b/constraintlayout/constraintlayout-compose/api/public_plus_experimental_current.txt
index 05f48b7..b9c3d1e 100644
--- a/constraintlayout/constraintlayout-compose/api/public_plus_experimental_current.txt
+++ b/constraintlayout/constraintlayout-compose/api/public_plus_experimental_current.txt
@@ -586,9 +586,6 @@
   public final class MotionDragHandlerKt {
   }
 
-  @kotlin.DslMarker public @interface MotionDslScope {
-  }
-
   public interface MotionItemsProvider {
     method public int count();
     method public kotlin.jvm.functions.Function0<kotlin.Unit> getContent(int index);
@@ -597,9 +594,6 @@
   }
 
   public final class MotionKt {
-    method @androidx.compose.runtime.Composable @androidx.constraintlayout.compose.ExperimentalMotionApi public static void Motion(optional androidx.compose.ui.Modifier modifier, kotlin.jvm.functions.Function1<? super androidx.constraintlayout.compose.MotionScope,kotlin.Unit> content);
-    method @androidx.compose.runtime.Composable @androidx.constraintlayout.compose.ExperimentalMotionApi public static kotlin.jvm.functions.Function1<androidx.constraintlayout.compose.MotionScope,kotlin.Unit> rememberMotionContent(kotlin.jvm.functions.Function1<? super androidx.constraintlayout.compose.MotionScope,kotlin.Unit> content);
-    method @androidx.compose.runtime.Composable @androidx.constraintlayout.compose.ExperimentalMotionApi public static java.util.List<kotlin.jvm.functions.Function2<androidx.constraintlayout.compose.MotionScope,java.lang.Integer,kotlin.Unit>> rememberMotionListItems(int count, kotlin.jvm.functions.Function2<? super androidx.constraintlayout.compose.MotionScope,? super java.lang.Integer,kotlin.Unit> content);
   }
 
   public enum MotionLayoutDebugFlags {
@@ -734,12 +728,6 @@
     method @androidx.constraintlayout.compose.ExperimentalMotionApi public static androidx.constraintlayout.compose.MotionScene MotionScene(kotlin.jvm.functions.Function1<? super androidx.constraintlayout.compose.MotionSceneScope,kotlin.Unit> motionSceneContent);
   }
 
-  @androidx.constraintlayout.compose.ExperimentalMotionApi @androidx.constraintlayout.compose.MotionDslScope public final class MotionScope implements androidx.compose.ui.layout.LookaheadLayoutScope {
-    ctor public MotionScope(androidx.compose.ui.layout.LookaheadLayoutScope lookaheadLayoutScope);
-    method @androidx.compose.runtime.Composable public void emit(java.util.List<? extends kotlin.jvm.functions.Function2<? super androidx.constraintlayout.compose.MotionScope,? super java.lang.Integer,kotlin.Unit>>);
-    method public androidx.compose.ui.Modifier motion(androidx.compose.ui.Modifier, optional androidx.compose.animation.core.AnimationSpec<java.lang.Float> animationSpec, optional boolean ignoreAxisChanges, optional kotlin.jvm.functions.Function1<? super androidx.constraintlayout.compose.MotionModifierScope,kotlin.Unit> motionDescription);
-  }
-
   @androidx.constraintlayout.compose.ExperimentalMotionApi public final class OnSwipe {
     ctor public OnSwipe(androidx.constraintlayout.compose.ConstrainedLayoutReference anchor, androidx.constraintlayout.compose.SwipeSide side, androidx.constraintlayout.compose.SwipeDirection direction, optional float dragScale, optional float dragThreshold, optional androidx.constraintlayout.compose.ConstrainedLayoutReference? dragAround, optional androidx.constraintlayout.compose.ConstrainedLayoutReference? limitBoundsTo, optional androidx.constraintlayout.compose.SwipeTouchUp onTouchUp, optional androidx.constraintlayout.compose.SwipeMode mode);
     method public androidx.constraintlayout.compose.ConstrainedLayoutReference component1();
diff --git a/constraintlayout/constraintlayout-compose/api/restricted_current.txt b/constraintlayout/constraintlayout-compose/api/restricted_current.txt
index d7d773c..c7ccd73 100644
--- a/constraintlayout/constraintlayout-compose/api/restricted_current.txt
+++ b/constraintlayout/constraintlayout-compose/api/restricted_current.txt
@@ -528,9 +528,6 @@
     method public androidx.constraintlayout.compose.MotionDragState onDragEnd(long velocity);
   }
 
-  @kotlin.DslMarker public @interface MotionDslScope {
-  }
-
   public interface MotionItemsProvider {
     method public int count();
     method public kotlin.jvm.functions.Function0<kotlin.Unit> getContent(int index);
diff --git a/constraintlayout/constraintlayout-compose/src/androidMain/kotlin/androidx/constraintlayout/compose/Motion.kt b/constraintlayout/constraintlayout-compose/src/androidMain/kotlin/androidx/constraintlayout/compose/Motion.kt
index 686ac0b3..662566b 100644
--- a/constraintlayout/constraintlayout-compose/src/androidMain/kotlin/androidx/constraintlayout/compose/Motion.kt
+++ b/constraintlayout/constraintlayout-compose/src/androidMain/kotlin/androidx/constraintlayout/compose/Motion.kt
@@ -93,7 +93,7 @@
 @ExperimentalMotionApi
 @OptIn(ExperimentalComposeUiApi::class)
 @Composable
-fun Motion(
+private fun Motion(
     modifier: Modifier = Modifier,
     content: @Composable MotionScope.() -> Unit
 ) {
@@ -125,7 +125,7 @@
 }
 
 @DslMarker
-annotation class MotionDslScope
+private annotation class MotionDslScope
 
 /**
  * Scope for the [Motion] Composable.
@@ -136,7 +136,7 @@
 @ExperimentalMotionApi
 @MotionDslScope
 @OptIn(ExperimentalComposeUiApi::class)
-class MotionScope(
+private class MotionScope(
     lookaheadLayoutScope: LookaheadLayoutScope
 ) : LookaheadLayoutScope by lookaheadLayoutScope {
     private var nextId: Int = 1000
@@ -360,7 +360,7 @@
  */
 @ExperimentalMotionApi
 @Composable
-fun rememberMotionContent(content: @Composable MotionScope.() -> Unit):
+private fun rememberMotionContent(content: @Composable MotionScope.() -> Unit):
     @Composable MotionScope.() -> Unit {
     return remember {
         movableContentOf(content)
@@ -376,7 +376,7 @@
  */
 @ExperimentalMotionApi
 @Composable
-fun rememberMotionListItems(
+private fun rememberMotionListItems(
     count: Int,
     content: @Composable MotionScope.(index: Int) -> Unit
 ): List<@Composable MotionScope.(index: Int) -> Unit> {
diff --git a/constraintlayout/constraintlayout-core/src/main/java/androidx/constraintlayout/core/state/ConstraintSetParser.java b/constraintlayout/constraintlayout-core/src/main/java/androidx/constraintlayout/core/state/ConstraintSetParser.java
index 2cdabc5..72db469 100644
--- a/constraintlayout/constraintlayout-core/src/main/java/androidx/constraintlayout/core/state/ConstraintSetParser.java
+++ b/constraintlayout/constraintlayout-core/src/main/java/androidx/constraintlayout/core/state/ConstraintSetParser.java
@@ -1094,31 +1094,16 @@
                     flow.setWrapMode(State.Wrap.getValueByString(wrapValue));
                     break;
                 case "vGap":
-                    String vGapValue = element.get(param).content();
-                    try {
-                        int value = Integer.parseInt(vGapValue);
-                        flow.setVerticalGap(value);
-                    } catch(NumberFormatException e) {
-
-                    }
+                    int vGapValue = element.get(param).getInt();
+                    flow.setVerticalGap(vGapValue);
                     break;
                 case "hGap":
-                    String hGapValue = element.get(param).content();
-                    try {
-                        int value = Integer.parseInt(hGapValue);
-                        flow.setHorizontalGap(value);
-                    } catch(NumberFormatException e) {
-
-                    }
+                    int hGapValue = element.get(param).getInt();
+                    flow.setHorizontalGap(hGapValue);
                     break;
                 case "maxElement":
-                    String maxElementValue = element.get(param).content();
-                    try {
-                        int value = Integer.parseInt(maxElementValue);
-                        flow.setMaxElementsWrap(value);
-                    } catch(NumberFormatException e) {
-
-                    }
+                    int maxElementValue = element.get(param).getInt();
+                    flow.setMaxElementsWrap(maxElementValue);
                     break;
                 case "padding":
                     CLElement paddingObject = element.get(param);
diff --git a/coordinatorlayout/coordinatorlayout/build.gradle b/coordinatorlayout/coordinatorlayout/build.gradle
index 8b23b1a..bc110f8 100644
--- a/coordinatorlayout/coordinatorlayout/build.gradle
+++ b/coordinatorlayout/coordinatorlayout/build.gradle
@@ -15,6 +15,7 @@
     androidTestImplementation(libs.testCore)
     androidTestImplementation(libs.testRunner)
     androidTestImplementation(libs.testRules)
+    androidTestImplementation(libs.material)
     androidTestImplementation(libs.espressoCore, excludes.espresso)
     androidTestImplementation(libs.bundles.espressoContrib, excludes.espresso)
     androidTestImplementation(libs.mockitoCore, excludes.bytebuddy)
@@ -38,6 +39,11 @@
     defaultConfig {
         multiDexEnabled = true
     }
+
+    testOptions {
+        animationsDisabled = true
+    }
+
     namespace "androidx.coordinatorlayout"
 }
 
diff --git a/coordinatorlayout/coordinatorlayout/src/androidTest/AndroidManifest.xml b/coordinatorlayout/coordinatorlayout/src/androidTest/AndroidManifest.xml
index b183770..0dedce5 100644
--- a/coordinatorlayout/coordinatorlayout/src/androidTest/AndroidManifest.xml
+++ b/coordinatorlayout/coordinatorlayout/src/androidTest/AndroidManifest.xml
@@ -14,7 +14,8 @@
   ~ See the License for the specific language governing permissions and
   ~ limitations under the License.
   -->
-<manifest xmlns:android="http://schemas.android.com/apk/res/android">
+<manifest
+    xmlns:android="http://schemas.android.com/apk/res/android">
 
     <application
         android:supportsRtl="true"
@@ -22,6 +23,12 @@
 
         <activity android:name="androidx.coordinatorlayout.widget.CoordinatorLayoutActivity"/>
 
+        <activity
+            android:exported="true"
+            android:theme="@style/Theme.AppCompat.Light"
+            android:name="androidx.coordinatorlayout.widget.CoordinatorWithNestedScrollViewsActivity"
+            />
+
         <activity android:name="androidx.coordinatorlayout.widget.DynamicCoordinatorLayoutActivity"/>
 
     </application>
diff --git a/coordinatorlayout/coordinatorlayout/src/androidTest/java/androidx/coordinatorlayout/testutils/AppBarStateChangedListener.java b/coordinatorlayout/coordinatorlayout/src/androidTest/java/androidx/coordinatorlayout/testutils/AppBarStateChangedListener.java
new file mode 100644
index 0000000..dad1a24
--- /dev/null
+++ b/coordinatorlayout/coordinatorlayout/src/androidTest/java/androidx/coordinatorlayout/testutils/AppBarStateChangedListener.java
@@ -0,0 +1,52 @@
+/*
+ * Copyright 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package androidx.coordinatorlayout.testutils;
+
+import com.google.android.material.appbar.AppBarLayout;
+/**
+ * Allows tests to determine if an AppBarLayout with a CollapsingToolbarLayout is expanded,
+ * animating, or collapsed.
+ */
+public abstract class AppBarStateChangedListener implements AppBarLayout.OnOffsetChangedListener {
+    public enum State { UNKNOWN, ANIMATING, EXPANDED, COLLAPSED }
+
+    private State mExistingState = State.UNKNOWN;
+
+    @Override
+    public final void onOffsetChanged(AppBarLayout appBarLayout, int verticalOffset) {
+        // Collapsed
+        if (Math.abs(verticalOffset) >= appBarLayout.getTotalScrollRange()) {
+            setStateAndNotify(appBarLayout, State.COLLAPSED);
+
+        // Expanded
+        } else if (verticalOffset == 0) {
+            setStateAndNotify(appBarLayout, State.EXPANDED);
+
+        // Animating
+        } else {
+            setStateAndNotify(appBarLayout, State.ANIMATING);
+        }
+    }
+
+    private void setStateAndNotify(AppBarLayout appBarLayout, State state) {
+        if (mExistingState != state) {
+            onStateChanged(appBarLayout, state);
+        }
+        mExistingState = state;
+    }
+
+    public abstract void onStateChanged(AppBarLayout appBarLayout, State state);
+}
diff --git a/coordinatorlayout/coordinatorlayout/src/androidTest/java/androidx/coordinatorlayout/testutils/NestedScrollViewActions.java b/coordinatorlayout/coordinatorlayout/src/androidTest/java/androidx/coordinatorlayout/testutils/NestedScrollViewActions.java
new file mode 100644
index 0000000..8c18bb9
--- /dev/null
+++ b/coordinatorlayout/coordinatorlayout/src/androidTest/java/androidx/coordinatorlayout/testutils/NestedScrollViewActions.java
@@ -0,0 +1,110 @@
+/*
+ * Copyright 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package androidx.coordinatorlayout.testutils;
+
+import static androidx.test.espresso.matcher.ViewMatchers.isAssignableFrom;
+import static androidx.test.espresso.matcher.ViewMatchers.isDescendantOfA;
+import static androidx.test.espresso.matcher.ViewMatchers.isDisplayingAtLeast;
+import static androidx.test.espresso.matcher.ViewMatchers.withEffectiveVisibility;
+
+import android.util.Log;
+import android.view.View;
+import android.view.ViewParent;
+
+import androidx.core.widget.NestedScrollView;
+import androidx.test.espresso.PerformException;
+import androidx.test.espresso.UiController;
+import androidx.test.espresso.ViewAction;
+import androidx.test.espresso.matcher.ViewMatchers;
+import androidx.test.espresso.util.HumanReadables;
+
+import org.hamcrest.Matcher;
+import org.hamcrest.Matchers;
+
+/**
+ * Supports scrolling with the NestedScrollView in Espresso.
+ */
+public final class NestedScrollViewActions {
+    private static final String TAG = NestedScrollViewActions.class.getSimpleName();
+
+    public static ViewAction scrollToTop() {
+        return new ViewAction() {
+            @Override
+            public Matcher<View> getConstraints() {
+                return Matchers.allOf(
+                        isDescendantOfA(isAssignableFrom(NestedScrollView.class)),
+                        withEffectiveVisibility(ViewMatchers.Visibility.VISIBLE)
+                );
+            }
+
+            @Override
+            public String getDescription() {
+                return "Find the first NestedScrollView parent (of the matched view) and "
+                        + "scroll to it.";
+            }
+
+            @Override
+            public void perform(UiController uiController, View view) {
+                if (isDisplayingAtLeast(90).matches(view)) {
+                    Log.i(TAG, "View is already completely displayed at top. Returning.");
+                    return;
+                }
+                try {
+                    NestedScrollView nestedScrollView = findFirstNestedScrollViewParent(view);
+
+                    if (nestedScrollView != null) {
+                        nestedScrollView.scrollTo(0, view.getTop());
+                    } else {
+                        throw new Exception("Can not find NestedScrollView parent.");
+                    }
+
+                } catch (Exception exception) {
+                    throw new PerformException.Builder()
+                            .withActionDescription(this.getDescription())
+                            .withViewDescription(HumanReadables.describe(view))
+                            .withCause(exception)
+                            .build();
+                }
+                uiController.loopMainThreadUntilIdle();
+
+                if (!isDisplayingAtLeast(90).matches(view)) {
+                    throw new PerformException.Builder()
+                            .withActionDescription(this.getDescription())
+                            .withViewDescription(HumanReadables.describe(view))
+                            .withCause(
+                                    new RuntimeException(
+                                            "Scrolling to view was attempted, but the view is"
+                                                    + "not displayed"))
+                            .build();
+                }
+            }
+        };
+    }
+
+    private static NestedScrollView findFirstNestedScrollViewParent(View view) {
+        ViewParent viewParent = view.getParent();
+
+        while (viewParent != null && !(viewParent.getClass() == NestedScrollView.class)) {
+            viewParent = viewParent.getParent();
+        }
+
+        if (viewParent == null) {
+            return null;
+        } else {
+            return (NestedScrollView) viewParent;
+        }
+    }
+}
diff --git a/coordinatorlayout/coordinatorlayout/src/androidTest/java/androidx/coordinatorlayout/widget/CoordinatorLayoutKeyEventTest.java b/coordinatorlayout/coordinatorlayout/src/androidTest/java/androidx/coordinatorlayout/widget/CoordinatorLayoutKeyEventTest.java
new file mode 100644
index 0000000..d31a488
--- /dev/null
+++ b/coordinatorlayout/coordinatorlayout/src/androidTest/java/androidx/coordinatorlayout/widget/CoordinatorLayoutKeyEventTest.java
@@ -0,0 +1,139 @@
+/*
+ * 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.coordinatorlayout.widget;
+
+import static androidx.test.espresso.Espresso.onView;
+import static androidx.test.espresso.action.ViewActions.pressKey;
+import static androidx.test.espresso.action.ViewActions.swipeUp;
+import static androidx.test.espresso.assertion.ViewAssertions.matches;
+import static androidx.test.espresso.matcher.ViewMatchers.isCompletelyDisplayed;
+import static androidx.test.espresso.matcher.ViewMatchers.withId;
+
+import static org.junit.Assert.assertEquals;
+
+import android.view.KeyEvent;
+
+import androidx.coordinatorlayout.test.R;
+import androidx.coordinatorlayout.testutils.AppBarStateChangedListener;
+import androidx.coordinatorlayout.testutils.NestedScrollViewActions;
+import androidx.test.ext.junit.rules.ActivityScenarioRule;
+import androidx.test.ext.junit.runners.AndroidJUnit4;
+import androidx.test.filters.LargeTest;
+import androidx.test.platform.app.InstrumentationRegistry;
+import androidx.testutils.PollingCheck;
+
+import com.google.android.material.appbar.AppBarLayout;
+
+import org.junit.Before;
+import org.junit.Rule;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+
+import java.util.concurrent.atomic.AtomicInteger;
+
+@SuppressWarnings({"unchecked", "rawtypes"})
+@LargeTest
+@RunWith(AndroidJUnit4.class)
+public class CoordinatorLayoutKeyEventTest {
+
+    // test rule
+    @Rule
+    public ActivityScenarioRule<CoordinatorWithNestedScrollViewsActivity> mActivityScenarioRule =
+            new ActivityScenarioRule(CoordinatorWithNestedScrollViewsActivity.class);
+
+    private AppBarLayout mAppBarLayout;
+    private AppBarStateChangedListener.State mAppBarState =
+            AppBarStateChangedListener.State.UNKNOWN;
+
+    @Before
+    public void setup() {
+        mActivityScenarioRule.getScenario().onActivity(activity -> {
+            mAppBarLayout = activity.mAppBarLayout;
+            mAppBarLayout.addOnOffsetChangedListener(new AppBarStateChangedListener() {
+                @Override
+                public void onStateChanged(AppBarLayout appBarLayout, State state) {
+                    mAppBarState = state;
+                }
+            });
+        });
+        InstrumentationRegistry.getInstrumentation().waitForIdleSync();
+    }
+
+    /*** Tests ***/
+    @Test
+    @LargeTest
+    public void isCollapsingToolbarExpanded_swipeDownMultipleKeysUp_isExpanded() {
+
+        onView(withId(R.id.top_nested_text)).check(matches(isCompletelyDisplayed()));
+
+        // Scrolls down content and collapses the CollapsingToolbarLayout in the AppBarLayout.
+        onView(withId(R.id.top_nested_text)).perform(swipeUp());
+        InstrumentationRegistry.getInstrumentation().waitForIdleSync();
+
+        // Espresso doesn't properly support swipeUp() with a CoordinatorLayout,
+        // AppBarLayout/CollapsingToolbarLayout, and NestedScrollView. From testing, it only
+        // handles waiting until the AppBarLayout/CollapsingToolbarLayout is finished with its
+        // transition, NOT waiting until the NestedScrollView is finished with its scrolling.
+        // This PollingCheck waits until the scroll is finished in the NestedScrollView.
+        AtomicInteger previousScroll = new AtomicInteger();
+        PollingCheck.waitFor(() -> {
+            AtomicInteger currentScroll = new AtomicInteger();
+
+            mActivityScenarioRule.getScenario().onActivity(activity -> {
+                currentScroll.set(activity.mNestedScrollView.getScrollY());
+            });
+
+            boolean isDone = currentScroll.get() == previousScroll.get();
+            previousScroll.set(currentScroll.get());
+
+            return isDone;
+        });
+
+        // Verifies the CollapsingToolbarLayout in the AppBarLayout is collapsed.
+        assertEquals(mAppBarState, AppBarStateChangedListener.State.COLLAPSED);
+
+        // Scrolls up to the top element in the NestedScrollView.
+        // NOTE: NestedScrollView requires a custom Action to work properly and the scroll does NOT
+        // impact the CoordinatorLayout's CollapsingToolbarLayout (which stays collapsed).
+        onView(withId(R.id.top_nested_text)).perform(NestedScrollViewActions.scrollToTop());
+        InstrumentationRegistry.getInstrumentation().waitForIdleSync();
+        onView(withId(R.id.top_nested_text)).check(matches(isCompletelyDisplayed()));
+
+        // First up keystroke gains focus (doesn't move any content).
+        onView(withId(R.id.top_nested_text)).perform(pressKey(KeyEvent.KEYCODE_DPAD_UP));
+        InstrumentationRegistry.getInstrumentation().waitForIdleSync();
+        onView(withId(R.id.top_nested_text)).check(matches(isCompletelyDisplayed()));
+
+        // This is a fail-safe in case the DPAD UP isn't making any changes, we break out of the
+        // loop.
+        float previousAppBarLayoutY = 0.0f;
+
+        // Performs a key press until the app bar is either expanded completely or no changes are
+        // made in the app bar between the previous call and the current call (failure case).
+        while (mAppBarState != AppBarStateChangedListener.State.EXPANDED
+                && (mAppBarLayout.getY() != previousAppBarLayoutY)
+        ) {
+            previousAppBarLayoutY = mAppBarLayout.getY();
+
+            // Partially expands the CollapsingToolbarLayout.
+            onView(withId(R.id.top_nested_text)).perform(pressKey(KeyEvent.KEYCODE_DPAD_UP));
+            InstrumentationRegistry.getInstrumentation().waitForIdleSync();
+        }
+
+        // Checks CollapsingToolbarLayout (in the AppBarLayout) is fully expanded.
+        assertEquals(mAppBarState, AppBarStateChangedListener.State.EXPANDED);
+    }
+}
diff --git a/coordinatorlayout/coordinatorlayout/src/androidTest/java/androidx/coordinatorlayout/widget/CoordinatorWithNestedScrollViewsActivity.java b/coordinatorlayout/coordinatorlayout/src/androidTest/java/androidx/coordinatorlayout/widget/CoordinatorWithNestedScrollViewsActivity.java
new file mode 100644
index 0000000..a8961bd
--- /dev/null
+++ b/coordinatorlayout/coordinatorlayout/src/androidTest/java/androidx/coordinatorlayout/widget/CoordinatorWithNestedScrollViewsActivity.java
@@ -0,0 +1,44 @@
+/*
+ * 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.coordinatorlayout.widget;
+
+import androidx.coordinatorlayout.BaseTestActivity;
+import androidx.coordinatorlayout.test.R;
+import androidx.core.widget.NestedScrollView;
+
+import com.google.android.material.appbar.AppBarLayout;
+import com.google.android.material.appbar.CollapsingToolbarLayout;
+
+public class CoordinatorWithNestedScrollViewsActivity extends BaseTestActivity {
+    AppBarLayout mAppBarLayout;
+    NestedScrollView mNestedScrollView;
+
+    @Override
+    protected int getContentViewLayoutResId() {
+        return R.layout.activity_coordinator_with_nested_scroll_views;
+    }
+
+    @Override
+    protected void onContentViewSet() {
+        mAppBarLayout = findViewById(R.id.app_bar_layout);
+        mNestedScrollView = findViewById(R.id.top_nested_scroll_view);
+
+        CollapsingToolbarLayout collapsingToolbarLayout =
+                findViewById(R.id.collapsing_toolbar_layout);
+
+        collapsingToolbarLayout.setTitle("Collapsing Bar Test");
+    }
+}
diff --git a/coordinatorlayout/coordinatorlayout/src/androidTest/res/layout/activity_coordinator_with_nested_scroll_views.xml b/coordinatorlayout/coordinatorlayout/src/androidTest/res/layout/activity_coordinator_with_nested_scroll_views.xml
new file mode 100644
index 0000000..f463373
--- /dev/null
+++ b/coordinatorlayout/coordinatorlayout/src/androidTest/res/layout/activity_coordinator_with_nested_scroll_views.xml
@@ -0,0 +1,115 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+  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.
+-->
+<androidx.coordinatorlayout.widget.CoordinatorLayout
+    xmlns:android="http://schemas.android.com/apk/res/android"
+    xmlns:app="http://schemas.android.com/apk/res-auto"
+    android:id="@+id/coordinator"
+    android:fitsSystemWindows="true"
+    android:layout_width="match_parent"
+    android:layout_height="match_parent">
+
+    <!-- App Bar -->
+    <com.google.android.material.appbar.AppBarLayout
+        android:id="@+id/app_bar_layout"
+        android:layout_width="match_parent"
+        android:layout_height="200dp">
+
+        <com.google.android.material.appbar.CollapsingToolbarLayout
+            android:id="@+id/collapsing_toolbar_layout"
+            android:layout_width="match_parent"
+            android:layout_height="match_parent"
+            app:expandedTitleMarginStart="48dp"
+            app:expandedTitleMarginEnd="64dp"
+            app:layout_scrollFlags="scroll|exitUntilCollapsed"
+            app:contentScrim="#00FFFF">
+
+            <View android:layout_width="match_parent"
+                android:layout_height="200dp"
+                android:background="#FF0000"/>
+
+            <androidx.appcompat.widget.Toolbar
+                android:id="@+id/toolbar"
+                android:layout_width="match_parent"
+                android:layout_height="?attr/actionBarSize"
+                app:layout_collapseMode="pin"/>
+        </com.google.android.material.appbar.CollapsingToolbarLayout>
+    </com.google.android.material.appbar.AppBarLayout>
+
+    <!-- Content -->
+    <androidx.core.widget.NestedScrollView
+        android:id="@+id/top_nested_scroll_view"
+        android:layout_width="match_parent"
+        android:layout_height="match_parent"
+        app:layout_behavior="@string/appbar_scrolling_view_behavior"
+        android:padding="16dp">
+
+        <LinearLayout
+            android:layout_width="match_parent"
+            android:layout_height="wrap_content"
+            android:orientation="vertical">
+
+            <TextView
+                android:id="@+id/top_nested_text"
+                android:layout_width="match_parent"
+                android:layout_height="wrap_content"
+                android:focusableInTouchMode="true"
+                android:background="#00FF00"
+                android:text="@string/medium_text"
+                android:textAppearance="?android:attr/textAppearance"/>
+
+            <androidx.core.widget.NestedScrollView
+                android:layout_width="match_parent"
+                android:layout_height="400dp"
+                android:padding="16dp">
+
+                <LinearLayout
+                    android:layout_width="match_parent"
+                    android:layout_height="wrap_content"
+                    android:orientation="vertical">
+
+                    <TextView
+                        android:layout_width="match_parent"
+                        android:layout_height="wrap_content"
+                        android:text="@string/short_text"
+                        android:textAppearance="?android:attr/textAppearance"/>
+
+                    <androidx.core.widget.NestedScrollView
+                        android:layout_width="match_parent"
+                        android:layout_height="200dp"
+                        android:padding="16dp">
+
+                        <TextView
+                            android:layout_width="match_parent"
+                            android:layout_height="wrap_content"
+                            android:text="@string/long_text"
+                            android:textAppearance="?android:attr/textAppearance"/>
+                    </androidx.core.widget.NestedScrollView>
+                    <TextView
+                        android:layout_width="match_parent"
+                        android:layout_height="wrap_content"
+                        android:text="@string/long_text"
+                        android:textAppearance="?android:attr/textAppearance"/>
+                </LinearLayout>
+            </androidx.core.widget.NestedScrollView>
+            <TextView
+                android:layout_width="match_parent"
+                android:layout_height="wrap_content"
+                android:text="@string/long_text"
+                android:textAppearance="?android:attr/textAppearance"/>
+        </LinearLayout>
+    </androidx.core.widget.NestedScrollView>
+</androidx.coordinatorlayout.widget.CoordinatorLayout>
diff --git a/coordinatorlayout/coordinatorlayout/src/androidTest/res/values/strings.xml b/coordinatorlayout/coordinatorlayout/src/androidTest/res/values/strings.xml
new file mode 100644
index 0000000..285994a
--- /dev/null
+++ b/coordinatorlayout/coordinatorlayout/src/androidTest/res/values/strings.xml
@@ -0,0 +1,27 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+  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.
+-->
+<resources>
+    <string name="short_text">a short string for  nested scroll view tests.</string>
+    <string name="medium_text">
+        This is some long text. It just keeps going. Look at it. Scroll it. Scroll the nested version of it. This is some long text. It just keeps going. Look at it. Scroll it. Scroll the nested version of it. This is some long text. It just keeps going. Look at it. Scroll it. Scroll the nested version of it. This is some long text. It just keeps going. Look at it. Scroll it. Scroll the nested version of it. This is some long text. It just keeps going. Look at it. Scroll it. Scroll the nested version of it. This is some long text. It just keeps going. Look at it. Scroll it. Scroll the nested version of it. This is some long text. It just keeps going. Look at it. Scroll it. Scroll the nested version of it. This is some long text. It just keeps going.
+    </string>
+    <string name="long_text">
+        This is some long text. It just keeps going. Look at it. Scroll it. Scroll the nested version of it. This is some long text. It just keeps going. Look at it. Scroll it. Scroll the nested version of it. This is some long text. It just keeps going. Look at it. Scroll it. Scroll the nested version of it. This is some long text. It just keeps going. Look at it. Scroll it. Scroll the nested version of it. This is some long text. It just keeps going. Look at it. Scroll it. Scroll the nested version of it. This is some long text. It just keeps going. Look at it. Scroll it. Scroll the nested version of it. This is some long text. It just keeps going. Look at it. Scroll it. Scroll the nested version of it. This is some long text. It just keeps going. Look at it. Scroll it. Scroll the nested version of it. This is some long text. It just keeps going. Look at it. Scroll it. Scroll the nested version of it. This is some long text. It just keeps going. Look at it. Scroll it. Scroll the nested version of it.
+        This is some long text. It just keeps going. Look at it. Scroll it. Scroll the nested version of it. This is some long text. It just keeps going. Look at it. Scroll it. Scroll the nested version of it. This is some long text. It just keeps going. Look at it. Scroll it. Scroll the nested version of it. This is some long text. It just keeps going. Look at it. Scroll it. Scroll the nested version of it. This is some long text. It just keeps going. Look at it. Scroll it. Scroll the nested version of it. This is some long text. It just keeps going. Look at it. Scroll it. Scroll the nested version of it. This is some long text. It just keeps going. Look at it. Scroll it. Scroll the nested version of it. This is some long text. It just keeps going. Look at it. Scroll it. Scroll the nested version of it. This is some long text. It just keeps going. Look at it. Scroll it. Scroll the nested version of it. This is some long text. It just keeps going. Look at it. Scroll it. Scroll the nested version of it.
+    </string>
+    <string name="appbar_scrolling_view_behavior" translatable="false">com.google.android.material.appbar.AppBarLayout$ScrollingViewBehavior</string>
+</resources>
diff --git a/coordinatorlayout/coordinatorlayout/src/main/java/androidx/coordinatorlayout/widget/CoordinatorLayout.java b/coordinatorlayout/coordinatorlayout/src/main/java/androidx/coordinatorlayout/widget/CoordinatorLayout.java
index dbb1b5e..71b0362 100644
--- a/coordinatorlayout/coordinatorlayout/src/main/java/androidx/coordinatorlayout/widget/CoordinatorLayout.java
+++ b/coordinatorlayout/coordinatorlayout/src/main/java/androidx/coordinatorlayout/widget/CoordinatorLayout.java
@@ -18,6 +18,7 @@
 
 import static androidx.annotation.RestrictTo.Scope.LIBRARY_GROUP_PREFIX;
 
+import android.annotation.SuppressLint;
 import android.content.Context;
 import android.content.res.Resources;
 import android.content.res.TypedArray;
@@ -37,6 +38,7 @@
 import android.util.Log;
 import android.util.SparseArray;
 import android.view.Gravity;
+import android.view.KeyEvent;
 import android.view.MotionEvent;
 import android.view.View;
 import android.view.ViewGroup;
@@ -1793,6 +1795,7 @@
     @SuppressWarnings("unchecked")
     public boolean onStartNestedScroll(@NonNull View child, @NonNull View target,
             int axes, int type) {
+
         boolean handled = false;
 
         final int childCount = getChildCount();
@@ -1935,6 +1938,140 @@
     }
 
     @Override
+    public boolean dispatchKeyEvent(
+            @SuppressLint("InvalidNullabilityOverride") @NonNull KeyEvent event
+    ) {
+        boolean handled = super.dispatchKeyEvent(event);
+
+        if (!handled) {
+            if (event.getAction() == KeyEvent.ACTION_DOWN) {
+                switch (event.getKeyCode()) {
+                    case KeyEvent.KEYCODE_DPAD_UP:
+                    case KeyEvent.KEYCODE_DPAD_DOWN:
+                    case KeyEvent.KEYCODE_SPACE:
+
+                        int yScrollDelta;
+
+                        if (event.getKeyCode() == KeyEvent.KEYCODE_SPACE) {
+                            if (event.isShiftPressed()) {
+                                // Places the CoordinatorLayout at the top of the available
+                                // content.
+                                // Note: The delta may represent a value that would overshoot the
+                                // top of the screen, but the children only use as much of the
+                                // delta as they can support, so it will always go exactly to the
+                                // top.
+                                yScrollDelta = -getFullContentHeight();
+                            } else {
+                                // Places the CoordinatorLayout at the bottom of the available
+                                // content.
+                                yScrollDelta = getFullContentHeight() - getHeight();
+                            }
+
+                        } else if (event.isAltPressed()) { // For UP and DOWN KeyEvents
+                            // Full page scroll
+                            yScrollDelta = getHeight();
+
+                        } else {
+                            // Regular arrow scroll
+                            yScrollDelta = (int) (getHeight() * 0.1f);
+                        }
+
+                        View focusedView = findDeepestFocusedChild(this);
+
+                        // Convert delta to negative if the key event is UP.
+                        if (event.getKeyCode() == KeyEvent.KEYCODE_DPAD_UP) {
+                            yScrollDelta = -yScrollDelta;
+                        }
+
+                        handled = manuallyTriggersNestedScrollFromKeyEvent(
+                                focusedView,
+                                yScrollDelta
+                        );
+
+                        break;
+                }
+            }
+        }
+
+        return handled;
+    }
+
+    private View findDeepestFocusedChild(View startingParentView) {
+        View focusedView = startingParentView;
+        while (focusedView != null) {
+            if (focusedView.isFocused()) {
+                return focusedView;
+            }
+            focusedView = focusedView instanceof ViewGroup
+                    ? ((ViewGroup) focusedView).getFocusedChild()
+                    : null;
+        }
+        return null;
+    }
+
+    /*
+     * Returns the height by adding up all children's heights (this is often larger than the screen
+     * height).
+     */
+    private int getFullContentHeight() {
+        int scrollRange = 0;
+        for (int i = 0; i < getChildCount(); i++) {
+            View child = getChildAt(i);
+            CoordinatorLayout.LayoutParams lp =
+                    (CoordinatorLayout.LayoutParams) child.getLayoutParams();
+            int childSize = child.getHeight() + lp.topMargin + lp.bottomMargin;
+            scrollRange += childSize;
+        }
+        return scrollRange;
+    }
+
+    /* This method only triggers when the focused child has passed on handling the
+     * KeyEvent to scroll (meaning the child is already scrolled as far as it can in that
+     * direction).
+     *
+     * For example, a key event should still expand/collapse a CollapsingAppBar event though the
+     * a NestedScrollView is at the top/bottom of its content.
+     */
+    private boolean manuallyTriggersNestedScrollFromKeyEvent(View focusedView, int yScrollDelta) {
+        boolean handled = false;
+
+        /* If this method is triggered and the event is triggered by a child, it means the
+         * child can't scroll any farther (and passed the event back up to the CoordinatorLayout),
+         * so the CoordinatorLayout triggers its own nested scroll to move content.
+         *
+         * To properly manually trigger onNestedScroll(), we need to
+         * 1. Call onStartNestedScroll() before onNestedScroll()
+         * 2. Call onNestedScroll() and pass this CoordinatorLayout as the child (because that is
+         * what we want to scroll
+         * 3. Call onStopNestedScroll() after onNestedScroll()
+         */
+        onStartNestedScroll(
+                this, // Passes the CoordinatorLayout itself, since we want it to scroll.
+                focusedView,
+                ViewCompat.SCROLL_AXIS_VERTICAL,
+                ViewCompat.TYPE_NON_TOUCH
+        );
+
+        onNestedScroll(
+                focusedView,
+                0,
+                0,
+                0,
+                yScrollDelta,
+                ViewCompat.TYPE_NON_TOUCH,
+                mBehaviorConsumed
+        );
+
+        onStopNestedScroll(focusedView, ViewCompat.TYPE_NON_TOUCH);
+
+        if (mBehaviorConsumed[1] > 0) {
+            handled = true;
+        }
+
+        return handled;
+    }
+
+    @Override
     public void onNestedPreScroll(@NonNull View target, int dx, int dy,
             @NonNull int[] consumed) {
         onNestedPreScroll(target, dx, dy, consumed, ViewCompat.TYPE_TOUCH);
@@ -3083,6 +3220,7 @@
         }
 
         void setNestedScrollAccepted(int type, boolean accept) {
+
             switch (type) {
                 case ViewCompat.TYPE_TOUCH:
                     mDidAcceptNestedScrollTouch = accept;
diff --git a/core/core-appdigest/build.gradle b/core/core-appdigest/build.gradle
index 3ab7a9b..a0d4af2 100644
--- a/core/core-appdigest/build.gradle
+++ b/core/core-appdigest/build.gradle
@@ -23,7 +23,7 @@
 
 dependencies {
     api("androidx.annotation:annotation:1.0.0")
-    implementation("androidx.core:core:1.8.0")
+    implementation("androidx.core:core:1.7.0")
     androidTestImplementation(libs.testExtJunit)
     androidTestImplementation(libs.testCore)
     androidTestImplementation(libs.testRunner)
diff --git a/core/core-appdigest/src/androidTest/java/androidx/core/appdigest/ChecksumsTest.java b/core/core-appdigest/src/androidTest/java/androidx/core/appdigest/ChecksumsTest.java
index b17233f..c261bc9 100644
--- a/core/core-appdigest/src/androidTest/java/androidx/core/appdigest/ChecksumsTest.java
+++ b/core/core-appdigest/src/androidTest/java/androidx/core/appdigest/ChecksumsTest.java
@@ -53,7 +53,6 @@
 import androidx.annotation.Nullable;
 import androidx.annotation.RequiresApi;
 import androidx.concurrent.futures.ResolvableFuture;
-import androidx.core.os.BuildCompat;
 import androidx.test.core.app.ApplicationProvider;
 import androidx.test.ext.junit.runners.AndroidJUnit4;
 import androidx.test.filters.LargeTest;
@@ -102,9 +101,6 @@
     private static final String TEST_V4_SPLIT3 = "HelloWorld5_xxhdpi-v4.apk";
     private static final String TEST_V4_SPLIT4 = "HelloWorld5_xxxhdpi-v4.apk";
 
-    private static final String[] SPLIT_NAMES = new String[] {null, "config.hdpi",
-            "config.mdpi", "config.xhdpi", "config.xxhdpi", "config.xxxhdpi"};
-
     private static final String TEST_FIXED_APK = "CtsPkgInstallTinyAppV2V3V4.apk";
     private static final String TEST_FIXED_APK_DIGESTS_FILE =
             "CtsPkgInstallTinyAppV2V3V4.digests";
@@ -116,8 +112,6 @@
             "CtsPkgInstallTinyAppV2V3V4-Sha512withEC.apk";
     private static final String TEST_FIXED_APK_VERITY = "CtsPkgInstallTinyAppV2V3V4-Verity.apk";
 
-    private static final String TEST_FIXED_APK_SHA256_VERITY =
-            "759626c33083fbf43215cb5b17156977d963d4c6850c0cb4e73162a665db560b";
     private static final String TEST_FIXED_APK_MD5 = "c19868da017dc01467169f8ea7c5bc57";
     private static final String TEST_FIXED_APK_V2_SHA256 =
             "1eec9e86e322b8d7e48e255fc3f2df2dbc91036e63982ff9850597c6a37bbeb3";
@@ -135,10 +129,6 @@
                     | TYPE_PARTIAL_MERKLE_ROOT_1M_SHA256 | TYPE_PARTIAL_MERKLE_ROOT_1M_SHA512;
     private static final char[] HEX_LOWER_CASE_DIGITS =
             {'0', '1', '2', '3', '4', '5', '6', '7', '8', '9', 'a', 'b', 'c', 'd', 'e', 'f'};
-
-    private static final boolean DEFAULT_VERITY = BuildCompat.isAtLeastU();
-    private static final boolean DEFAULT_V3 = (Build.VERSION.SDK_INT >= 31);
-
     private Context mContext;
     private Executor mExecutor;
 
@@ -277,42 +267,19 @@
         assertFalse(isAppInstalled(FIXED_PACKAGE_NAME));
     }
 
-    private void checkDefaultChecksums(Checksum[] checksums, String[] names) {
-        checkDefaultChecksums(checksums, names, /*hashes=*/null);
-    }
-
-    private void checkDefaultChecksums(Checksum[] checksums, String[] names, String[] hashes) {
-        assertNotNull(checksums);
-        int idx = 0, hashIdx = 0;
-        for (int i = 0, size = names.length; i < size; ++i) {
-            if (DEFAULT_VERITY) {
-                assertEquals(names[i], checksums[idx].getSplitName());
-                assertEquals(TYPE_WHOLE_MERKLE_ROOT_4K_SHA256, checksums[idx].getType());
-                if (hashes != null) {
-                    assertEquals(hashes[hashIdx], bytesToHexString(checksums[idx].getValue()));
-                }
-                ++idx;
-            }
-            ++hashIdx;
-            if (DEFAULT_V3) {
-                assertEquals(names[i], checksums[idx].getSplitName());
-                assertEquals(TYPE_PARTIAL_MERKLE_ROOT_1M_SHA256, checksums[idx].getType());
-                if (hashes != null) {
-                    assertEquals(hashes[hashIdx], bytesToHexString(checksums[idx].getValue()));
-                }
-                ++idx;
-            }
-            ++hashIdx;
-        }
-    }
-
     @SmallTest
     @Test
     @SdkSuppress(maxSdkVersion = 33) // b/262909049: Failing on SDK 34 // b/262909049: Failing on SDK 34
     public void testDefaultChecksums() throws Exception {
-        checkDefaultChecksums(
-                getChecksums(V2V3_PACKAGE_NAME, true, 0, TRUST_NONE),
-                new String[] {null});
+        Checksum[] checksums = getChecksums(V2V3_PACKAGE_NAME, true, 0, TRUST_NONE);
+        assertNotNull(checksums);
+        if (Build.VERSION.SDK_INT >= 31) {
+            assertEquals(1, checksums.length);
+            assertEquals(checksums[0].getType(),
+                    android.content.pm.Checksum.TYPE_PARTIAL_MERKLE_ROOT_1M_SHA256);
+        } else {
+            assertEquals(0, checksums.length);
+        }
     }
 
     @SmallTest
@@ -331,9 +298,26 @@
                 TEST_V4_SPLIT3, TEST_V4_SPLIT4});
         assertTrue(isAppInstalled(V4_PACKAGE_NAME));
 
-        checkDefaultChecksums(
-                getChecksums(V4_PACKAGE_NAME, true, 0, TRUST_NONE),
-                SPLIT_NAMES);
+        Checksum[] checksums = getChecksums(V4_PACKAGE_NAME, true, 0, TRUST_NONE);
+        assertNotNull(checksums);
+        if (Build.VERSION.SDK_INT >= 31) {
+            assertEquals(checksums.length, 6);
+            // v2/v3 signature use 1M merkle tree.
+            assertEquals(null, checksums[0].getSplitName());
+            assertEquals(TYPE_PARTIAL_MERKLE_ROOT_1M_SHA256, checksums[0].getType());
+            assertEquals("config.hdpi", checksums[1].getSplitName());
+            assertEquals(TYPE_PARTIAL_MERKLE_ROOT_1M_SHA256, checksums[1].getType());
+            assertEquals("config.mdpi", checksums[2].getSplitName());
+            assertEquals(TYPE_PARTIAL_MERKLE_ROOT_1M_SHA256, checksums[2].getType());
+            assertEquals("config.xhdpi", checksums[3].getSplitName());
+            assertEquals(TYPE_PARTIAL_MERKLE_ROOT_1M_SHA256, checksums[3].getType());
+            assertEquals("config.xxhdpi", checksums[4].getSplitName());
+            assertEquals(TYPE_PARTIAL_MERKLE_ROOT_1M_SHA256, checksums[4].getType());
+            assertEquals("config.xxxhdpi", checksums[5].getSplitName());
+            assertEquals(TYPE_PARTIAL_MERKLE_ROOT_1M_SHA256, checksums[5].getType());
+        } else {
+            assertEquals(0, checksums.length);
+        }
     }
 
     @SdkSuppress(minSdkVersion = 29)
@@ -357,34 +341,74 @@
                 TEST_V4_SPLIT3, TEST_V4_SPLIT4});
         assertTrue(isAppInstalled(V4_PACKAGE_NAME));
 
-        String[] hashes = new String[] {
-                "ce4ad41be1191ab3cdfef09ab6fb3c5d057e15cb3553661b393f770d9149f1cc",
-                "336a47c278f6b6c22abffefa6a62971fd0bd718d6947143e6ed1f6f6126a8196",
-                "17fe9f85e6f29a7354932002c8bc4cb829e1f4acf7f30626bd298c810bb13215",
-                "71a0b0ac5970def7ad80071c909be1e446174a9b39ea5cbf3004db05f87bcc4b",
-                "cf6eaee309cf906df5519b9a449ab136841cec62857e283fb4fd20dcd2ea14aa",
-                "e7c51a01794d33e13d005b62e5ae96a39215bc588e0a2ef8f6161e1e360a17cc",
-        };
-
         Checksum[] checksums = getChecksums(V4_PACKAGE_NAME, true, TYPE_WHOLE_SHA256,
                 TRUST_NONE);
         assertNotNull(checksums);
-        int idx = 0;
-        for (int i = 0, size = SPLIT_NAMES.length; i < size; ++i) {
-            assertEquals(SPLIT_NAMES[i], checksums[idx].getSplitName());
-            if (DEFAULT_VERITY) {
-                assertEquals(SPLIT_NAMES[i], checksums[idx].getSplitName());
-                assertEquals(TYPE_WHOLE_MERKLE_ROOT_4K_SHA256, checksums[idx].getType());
-                ++idx;
-            }
-            assertEquals(TYPE_WHOLE_SHA256, checksums[idx].getType());
-            assertEquals(hashes[i], bytesToHexString(checksums[idx].getValue()));
-            ++idx;
-            if (DEFAULT_V3) {
-                assertEquals(SPLIT_NAMES[i], checksums[idx].getSplitName());
-                assertEquals(TYPE_PARTIAL_MERKLE_ROOT_1M_SHA256, checksums[idx].getType());
-                ++idx;
-            }
+        if (Build.VERSION.SDK_INT >= 31) {
+            assertEquals(checksums.length, 12);
+            // v2/v3 signature use 1M merkle tree.
+            assertEquals(null, checksums[0].getSplitName());
+            assertEquals(TYPE_WHOLE_SHA256, checksums[0].getType());
+            assertEquals(bytesToHexString(checksums[0].getValue()),
+                    "ce4ad41be1191ab3cdfef09ab6fb3c5d057e15cb3553661b393f770d9149f1cc");
+            assertEquals(null, checksums[1].getSplitName());
+            assertEquals(TYPE_PARTIAL_MERKLE_ROOT_1M_SHA256, checksums[1].getType());
+            assertEquals(checksums[2].getSplitName(), "config.hdpi");
+            assertEquals(checksums[2].getType(), TYPE_WHOLE_SHA256);
+            assertEquals(bytesToHexString(checksums[2].getValue()),
+                    "336a47c278f6b6c22abffefa6a62971fd0bd718d6947143e6ed1f6f6126a8196");
+            assertEquals("config.hdpi", checksums[3].getSplitName());
+            assertEquals(TYPE_PARTIAL_MERKLE_ROOT_1M_SHA256, checksums[3].getType());
+            assertEquals(checksums[4].getSplitName(), "config.mdpi");
+            assertEquals(checksums[4].getType(), TYPE_WHOLE_SHA256);
+            assertEquals(bytesToHexString(checksums[4].getValue()),
+                    "17fe9f85e6f29a7354932002c8bc4cb829e1f4acf7f30626bd298c810bb13215");
+            assertEquals("config.mdpi", checksums[5].getSplitName());
+            assertEquals(TYPE_PARTIAL_MERKLE_ROOT_1M_SHA256, checksums[5].getType());
+            assertEquals(checksums[6].getSplitName(), "config.xhdpi");
+            assertEquals(checksums[6].getType(), TYPE_WHOLE_SHA256);
+            assertEquals(bytesToHexString(checksums[6].getValue()),
+                    "71a0b0ac5970def7ad80071c909be1e446174a9b39ea5cbf3004db05f87bcc4b");
+            assertEquals("config.xhdpi", checksums[7].getSplitName());
+            assertEquals(TYPE_PARTIAL_MERKLE_ROOT_1M_SHA256, checksums[7].getType());
+            assertEquals(checksums[8].getSplitName(), "config.xxhdpi");
+            assertEquals(checksums[8].getType(), TYPE_WHOLE_SHA256);
+            assertEquals(bytesToHexString(checksums[8].getValue()),
+                    "cf6eaee309cf906df5519b9a449ab136841cec62857e283fb4fd20dcd2ea14aa");
+            assertEquals("config.xxhdpi", checksums[9].getSplitName());
+            assertEquals(TYPE_PARTIAL_MERKLE_ROOT_1M_SHA256, checksums[9].getType());
+            assertEquals(checksums[10].getSplitName(), "config.xxxhdpi");
+            assertEquals(checksums[10].getType(), TYPE_WHOLE_SHA256);
+            assertEquals(bytesToHexString(checksums[10].getValue()),
+                    "e7c51a01794d33e13d005b62e5ae96a39215bc588e0a2ef8f6161e1e360a17cc");
+            assertEquals("config.xxxhdpi", checksums[11].getSplitName());
+            assertEquals(TYPE_PARTIAL_MERKLE_ROOT_1M_SHA256, checksums[11].getType());
+        } else {
+            assertEquals(6, checksums.length);
+            assertEquals(checksums[0].getSplitName(), null);
+            assertEquals(checksums[0].getType(), TYPE_WHOLE_SHA256);
+            assertEquals(bytesToHexString(checksums[0].getValue()),
+                    "ce4ad41be1191ab3cdfef09ab6fb3c5d057e15cb3553661b393f770d9149f1cc");
+            assertEquals(checksums[1].getSplitName(), "config.hdpi");
+            assertEquals(checksums[1].getType(), TYPE_WHOLE_SHA256);
+            assertEquals(bytesToHexString(checksums[1].getValue()),
+                    "336a47c278f6b6c22abffefa6a62971fd0bd718d6947143e6ed1f6f6126a8196");
+            assertEquals(checksums[2].getSplitName(), "config.mdpi");
+            assertEquals(checksums[2].getType(), TYPE_WHOLE_SHA256);
+            assertEquals(bytesToHexString(checksums[2].getValue()),
+                    "17fe9f85e6f29a7354932002c8bc4cb829e1f4acf7f30626bd298c810bb13215");
+            assertEquals(checksums[3].getSplitName(), "config.xhdpi");
+            assertEquals(checksums[3].getType(), TYPE_WHOLE_SHA256);
+            assertEquals(bytesToHexString(checksums[3].getValue()),
+                    "71a0b0ac5970def7ad80071c909be1e446174a9b39ea5cbf3004db05f87bcc4b");
+            assertEquals(checksums[4].getSplitName(), "config.xxhdpi");
+            assertEquals(checksums[4].getType(), TYPE_WHOLE_SHA256);
+            assertEquals(bytesToHexString(checksums[4].getValue()),
+                    "cf6eaee309cf906df5519b9a449ab136841cec62857e283fb4fd20dcd2ea14aa");
+            assertEquals(checksums[5].getSplitName(), "config.xxxhdpi");
+            assertEquals(checksums[5].getType(), TYPE_WHOLE_SHA256);
+            assertEquals(bytesToHexString(checksums[5].getValue()),
+                    "e7c51a01794d33e13d005b62e5ae96a39215bc588e0a2ef8f6161e1e360a17cc");
         }
     }
 
@@ -413,10 +437,18 @@
         installPackage(TEST_FIXED_APK);
         assertTrue(isAppInstalled(FIXED_PACKAGE_NAME));
 
-        checkDefaultChecksums(
-                getChecksums(FIXED_PACKAGE_NAME, true, 0, TRUST_NONE),
-                new String[] {null},
-                new String[] {TEST_FIXED_APK_SHA256_VERITY, TEST_FIXED_APK_V2_SHA256});
+        Checksum[] checksums = getChecksums(FIXED_PACKAGE_NAME, true, 0,
+                TRUST_NONE);
+        assertNotNull(checksums);
+        if (Build.VERSION.SDK_INT >= 31) {
+            assertEquals(1, checksums.length);
+            // v2/v3 signature use 1M merkle tree.
+            assertEquals(TYPE_PARTIAL_MERKLE_ROOT_1M_SHA256, checksums[0].getType());
+            assertEquals(TEST_FIXED_APK_V2_SHA256, bytesToHexString(checksums[0].getValue()));
+            assertNull(checksums[0].getInstallerCertificate());
+        } else {
+            assertEquals(0, checksums.length);
+        }
     }
 
     @SdkSuppress(minSdkVersion = 29)
@@ -441,14 +473,7 @@
         Checksum[] checksums = getChecksums(FIXED_PACKAGE_NAME, true, 0,
                 TRUST_NONE);
         assertNotNull(checksums);
-        int idx = 0;
-        if (DEFAULT_VERITY) {
-            assertEquals(TYPE_WHOLE_MERKLE_ROOT_4K_SHA256, checksums[idx].getType());
-            assertEquals(bytesToHexString(checksums[idx].getValue()),
-                    "0b9bd6ef683e0c4e8940aba6460382b33e607c0fcf487f3dc6a44b715615d166");
-            assertNull(checksums[idx].getInstallerCertificate());
-            ++idx;
-        }
+        assertEquals(0, checksums.length);
     }
 
     @SdkSuppress(minSdkVersion = 29)
@@ -473,21 +498,16 @@
         Checksum[] checksums = getChecksums(FIXED_PACKAGE_NAME, true, 0,
                 TRUST_NONE);
         assertNotNull(checksums);
-        int idx = 0;
-        if (DEFAULT_VERITY) {
-            assertEquals(TYPE_WHOLE_MERKLE_ROOT_4K_SHA256, checksums[idx].getType());
-            assertEquals(bytesToHexString(checksums[idx].getValue()),
-                    "15090bc8de638803246d63a7dae61808bb773a1f570f26157fe1df79f9b388a9");
-            assertNull(checksums[idx].getInstallerCertificate());
-            ++idx;
-        }
-        if (DEFAULT_V3) {
-            assertEquals(TYPE_PARTIAL_MERKLE_ROOT_1M_SHA512, checksums[idx].getType());
-            assertEquals(bytesToHexString(checksums[idx].getValue()),
+        if (Build.VERSION.SDK_INT >= 31) {
+            assertEquals(1, checksums.length);
+            // v2/v3 signature use 1M merkle tree.
+            assertEquals(TYPE_PARTIAL_MERKLE_ROOT_1M_SHA512, checksums[0].getType());
+            assertEquals(bytesToHexString(checksums[0].getValue()),
                     "6b866e8a54a3e358dfc20007960fb96123845f6c6d6c45f5fddf88150d71677f"
                             + "4c3081a58921c88651f7376118aca312cf764b391cdfb8a18c6710f9f27916a0");
-            assertNull(checksums[idx].getInstallerCertificate());
-            ++idx;
+            assertNull(checksums[0].getInstallerCertificate());
+        } else {
+            assertEquals(0, checksums.length);
         }
     }
 
@@ -513,15 +533,8 @@
         Checksum[] checksums = getChecksums(FIXED_PACKAGE_NAME, true, 0,
                 TRUST_NONE);
         assertNotNull(checksums);
-        int idx = 0;
-        if (DEFAULT_VERITY) {
-            assertEquals(TYPE_WHOLE_MERKLE_ROOT_4K_SHA256, checksums[idx].getType());
-            assertEquals(bytesToHexString(checksums[idx].getValue()),
-                    "ed28813663aaf1443a843a6a4fba0518ad544bc5af97720ad3d16fb8208590b0");
-            assertNull(checksums[idx].getInstallerPackageName());
-            assertNull(checksums[idx].getInstallerCertificate());
-            ++idx;
-        }
+        // No usable hashes as verity-in-v2-signature does not cover the whole file.
+        assertEquals(0, checksums.length);
     }
 
     @SdkSuppress(minSdkVersion = 29)
@@ -739,21 +752,11 @@
 
         Checksum[] checksums = getChecksums(FIXED_PACKAGE_NAME, true, 0, TRUST_NONE);
         assertNotNull(checksums);
-        int idx = 0;
-        if (DEFAULT_VERITY) {
-            assertEquals(TYPE_WHOLE_MERKLE_ROOT_4K_SHA256, checksums[idx].getType());
-            assertEquals(bytesToHexString(checksums[idx].getValue()), TEST_FIXED_APK_SHA256_VERITY);
-            assertNull(checksums[idx].getInstallerPackageName());
-            assertNull(checksums[idx].getInstallerCertificate());
-            ++idx;
-        }
-        if (DEFAULT_V3) {
-            assertEquals(TYPE_PARTIAL_MERKLE_ROOT_1M_SHA256, checksums[idx].getType());
-            assertEquals(bytesToHexString(checksums[idx].getValue()), TEST_FIXED_APK_V2_SHA256);
-            assertNull(checksums[idx].getInstallerPackageName());
-            assertNull(checksums[idx].getInstallerCertificate());
-            ++idx;
-        }
+        assertEquals(1, checksums.length);
+        assertEquals(checksums[0].getType(), TYPE_PARTIAL_MERKLE_ROOT_1M_SHA256);
+        assertEquals(bytesToHexString(checksums[0].getValue()), TEST_FIXED_APK_V2_SHA256);
+        assertNull(checksums[0].getInstallerPackageName());
+        assertNull(checksums[0].getInstallerCertificate());
     }
 
     @SdkSuppress(minSdkVersion = 31)
@@ -776,35 +779,25 @@
         final Certificate certificate = InstallerApi31.getInstallerCertificate(mContext);
 
         Checksum[] checksums = getChecksums(FIXED_PACKAGE_NAME, true, 0, TRUST_ALL);
-        assertNotNull(checksums);
 
-        int idx = 0;
-        if (DEFAULT_VERITY) {
-            assertEquals(TYPE_WHOLE_MERKLE_ROOT_4K_SHA256, checksums[idx].getType());
-            assertEquals(bytesToHexString(checksums[idx].getValue()), TEST_FIXED_APK_SHA256_VERITY);
-            assertNull(checksums[idx].getInstallerPackageName());
-            assertNull(checksums[idx].getInstallerCertificate());
-            ++idx;
-        }
-        assertEquals(checksums[idx].getType(), TYPE_WHOLE_MD5);
-        assertEquals(bytesToHexString(checksums[idx].getValue()), TEST_FIXED_APK_MD5);
-        assertEquals(checksums[idx].getSplitName(), null);
-        assertEquals(checksums[idx].getInstallerPackageName(), INSTALLER_PACKAGE_NAME);
-        assertEquals(checksums[idx].getInstallerCertificate(), certificate);
-        ++idx;
-        assertEquals(checksums[idx].getType(), TYPE_WHOLE_SHA256);
-        assertEquals(bytesToHexString(checksums[idx].getValue()), TEST_FIXED_APK_SHA256);
-        assertEquals(checksums[idx].getSplitName(), null);
-        assertEquals(checksums[idx].getInstallerPackageName(), INSTALLER_PACKAGE_NAME);
-        assertEquals(checksums[idx].getInstallerCertificate(), certificate);
-        ++idx;
-        if (DEFAULT_V3) {
-            assertEquals(TYPE_PARTIAL_MERKLE_ROOT_1M_SHA256, checksums[idx].getType());
-            assertEquals(bytesToHexString(checksums[idx].getValue()), TEST_FIXED_APK_V2_SHA256);
-            assertNull(checksums[idx].getInstallerPackageName());
-            assertNull(checksums[idx].getInstallerCertificate());
-            ++idx;
-        }
+        assertNotNull(checksums);
+        // installer provided.
+        assertEquals(3, checksums.length);
+        assertEquals(checksums[0].getType(), TYPE_WHOLE_MD5);
+        assertEquals(bytesToHexString(checksums[0].getValue()), TEST_FIXED_APK_MD5);
+        assertEquals(checksums[0].getSplitName(), null);
+        assertEquals(checksums[0].getInstallerPackageName(), INSTALLER_PACKAGE_NAME);
+        assertEquals(checksums[0].getInstallerCertificate(), certificate);
+        assertEquals(checksums[1].getType(), TYPE_WHOLE_SHA256);
+        assertEquals(bytesToHexString(checksums[1].getValue()), TEST_FIXED_APK_SHA256);
+        assertEquals(checksums[1].getSplitName(), null);
+        assertEquals(checksums[1].getInstallerPackageName(), INSTALLER_PACKAGE_NAME);
+        assertEquals(checksums[1].getInstallerCertificate(), certificate);
+        assertEquals(checksums[2].getType(), TYPE_PARTIAL_MERKLE_ROOT_1M_SHA256);
+        assertEquals(bytesToHexString(checksums[2].getValue()), TEST_FIXED_APK_V2_SHA256);
+        assertEquals(checksums[2].getSplitName(), null);
+        assertNull(checksums[2].getInstallerPackageName());
+        assertNull(checksums[2].getInstallerCertificate());
     }
 
     @SdkSuppress(minSdkVersion = 31)
@@ -852,36 +845,24 @@
         installApkWithChecksums(signature);
 
         Checksum[] checksums = getChecksums(FIXED_PACKAGE_NAME, true, 0, TRUST_ALL);
+
         assertNotNull(checksums);
-        int idx = 0;
-        if (DEFAULT_VERITY) {
-            assertEquals(checksums[idx].getType(), TYPE_WHOLE_MERKLE_ROOT_4K_SHA256);
-            assertEquals(bytesToHexString(checksums[idx].getValue()), TEST_FIXED_APK_SHA256_VERITY);
-            assertEquals(checksums[idx].getSplitName(), null);
-            assertNull(checksums[idx].getInstallerPackageName());
-            assertNull(checksums[idx].getInstallerCertificate());
-            ++idx;
-        }
-        assertEquals(checksums[idx].getType(), TYPE_WHOLE_MD5);
-        assertEquals(bytesToHexString(checksums[idx].getValue()), TEST_FIXED_APK_MD5);
-        assertEquals(checksums[idx].getSplitName(), null);
-        assertEquals(checksums[idx].getInstallerPackageName(), INSTALLER_PACKAGE_NAME);
-        assertEquals(checksums[idx].getInstallerCertificate(), certificate);
-        ++idx;
-        assertEquals(checksums[idx].getType(), TYPE_WHOLE_SHA256);
-        assertEquals(bytesToHexString(checksums[idx].getValue()), TEST_FIXED_APK_SHA256);
-        assertEquals(checksums[idx].getSplitName(), null);
-        assertEquals(checksums[idx].getInstallerPackageName(), INSTALLER_PACKAGE_NAME);
-        assertEquals(checksums[idx].getInstallerCertificate(), certificate);
-        ++idx;
-        if (DEFAULT_V3) {
-            assertEquals(checksums[idx].getType(), TYPE_PARTIAL_MERKLE_ROOT_1M_SHA256);
-            assertEquals(bytesToHexString(checksums[idx].getValue()), TEST_FIXED_APK_V2_SHA256);
-            assertEquals(checksums[idx].getSplitName(), null);
-            assertNull(checksums[idx].getInstallerPackageName());
-            assertNull(checksums[idx].getInstallerCertificate());
-            ++idx;
-        }
+        assertEquals(3, checksums.length);
+        assertEquals(checksums[0].getType(), TYPE_WHOLE_MD5);
+        assertEquals(bytesToHexString(checksums[0].getValue()), TEST_FIXED_APK_MD5);
+        assertEquals(checksums[0].getSplitName(), null);
+        assertEquals(checksums[0].getInstallerPackageName(), INSTALLER_PACKAGE_NAME);
+        assertEquals(checksums[0].getInstallerCertificate(), certificate);
+        assertEquals(checksums[1].getType(), TYPE_WHOLE_SHA256);
+        assertEquals(bytesToHexString(checksums[1].getValue()), TEST_FIXED_APK_SHA256);
+        assertEquals(checksums[1].getSplitName(), null);
+        assertEquals(checksums[1].getInstallerPackageName(), INSTALLER_PACKAGE_NAME);
+        assertEquals(checksums[1].getInstallerCertificate(), certificate);
+        assertEquals(checksums[2].getType(), TYPE_PARTIAL_MERKLE_ROOT_1M_SHA256);
+        assertEquals(bytesToHexString(checksums[2].getValue()), TEST_FIXED_APK_V2_SHA256);
+        assertEquals(checksums[2].getSplitName(), null);
+        assertNull(checksums[2].getInstallerPackageName());
+        assertNull(checksums[2].getInstallerCertificate());
     }
 
     @SdkSuppress(minSdkVersion = 31)
diff --git a/core/core/api/current.txt b/core/core/api/current.txt
index 6d6d72f..3f5d92d 100644
--- a/core/core/api/current.txt
+++ b/core/core/api/current.txt
@@ -3136,8 +3136,9 @@
     method public void setSystemBarsBehavior(int);
     method public void show(int);
     method @Deprecated @RequiresApi(30) public static androidx.core.view.WindowInsetsControllerCompat toWindowInsetsControllerCompat(android.view.WindowInsetsController);
-    field public static final int BEHAVIOR_SHOW_BARS_BY_SWIPE = 1; // 0x1
-    field public static final int BEHAVIOR_SHOW_BARS_BY_TOUCH = 0; // 0x0
+    field public static final int BEHAVIOR_DEFAULT = 1; // 0x1
+    field @Deprecated public static final int BEHAVIOR_SHOW_BARS_BY_SWIPE = 1; // 0x1
+    field @Deprecated public static final int BEHAVIOR_SHOW_BARS_BY_TOUCH = 0; // 0x0
     field public static final int BEHAVIOR_SHOW_TRANSIENT_BARS_BY_SWIPE = 2; // 0x2
   }
 
diff --git a/core/core/api/public_plus_experimental_current.txt b/core/core/api/public_plus_experimental_current.txt
index affd6fa..9df9106 100644
--- a/core/core/api/public_plus_experimental_current.txt
+++ b/core/core/api/public_plus_experimental_current.txt
@@ -3142,8 +3142,9 @@
     method public void setSystemBarsBehavior(int);
     method public void show(int);
     method @Deprecated @RequiresApi(30) public static androidx.core.view.WindowInsetsControllerCompat toWindowInsetsControllerCompat(android.view.WindowInsetsController);
-    field public static final int BEHAVIOR_SHOW_BARS_BY_SWIPE = 1; // 0x1
-    field public static final int BEHAVIOR_SHOW_BARS_BY_TOUCH = 0; // 0x0
+    field public static final int BEHAVIOR_DEFAULT = 1; // 0x1
+    field @Deprecated public static final int BEHAVIOR_SHOW_BARS_BY_SWIPE = 1; // 0x1
+    field @Deprecated public static final int BEHAVIOR_SHOW_BARS_BY_TOUCH = 0; // 0x0
     field public static final int BEHAVIOR_SHOW_TRANSIENT_BARS_BY_SWIPE = 2; // 0x2
   }
 
diff --git a/core/core/api/restricted_current.txt b/core/core/api/restricted_current.txt
index 365107b..51353767 100644
--- a/core/core/api/restricted_current.txt
+++ b/core/core/api/restricted_current.txt
@@ -3596,8 +3596,9 @@
     method public void setSystemBarsBehavior(int);
     method public void show(@androidx.core.view.WindowInsetsCompat.Type.InsetsType int);
     method @Deprecated @RequiresApi(30) public static androidx.core.view.WindowInsetsControllerCompat toWindowInsetsControllerCompat(android.view.WindowInsetsController);
-    field public static final int BEHAVIOR_SHOW_BARS_BY_SWIPE = 1; // 0x1
-    field public static final int BEHAVIOR_SHOW_BARS_BY_TOUCH = 0; // 0x0
+    field public static final int BEHAVIOR_DEFAULT = 1; // 0x1
+    field @Deprecated public static final int BEHAVIOR_SHOW_BARS_BY_SWIPE = 1; // 0x1
+    field @Deprecated public static final int BEHAVIOR_SHOW_BARS_BY_TOUCH = 0; // 0x0
     field public static final int BEHAVIOR_SHOW_TRANSIENT_BARS_BY_SWIPE = 2; // 0x2
   }
 
diff --git a/core/core/src/androidTest/java/androidx/core/view/WindowInsetsControllerCompatActivityTest.kt b/core/core/src/androidTest/java/androidx/core/view/WindowInsetsControllerCompatActivityTest.kt
index b7bb15f..6a40e06 100644
--- a/core/core/src/androidTest/java/androidx/core/view/WindowInsetsControllerCompatActivityTest.kt
+++ b/core/core/src/androidTest/java/androidx/core/view/WindowInsetsControllerCompatActivityTest.kt
@@ -376,12 +376,31 @@
     }
 
     @Test
-    // minSdkVersion = 21 due to b/189492236
-    @SdkSuppress(minSdkVersion = 21, maxSdkVersion = 29) // Flag deprecated in 30+
-    public fun systemBarsBehavior_swipe() {
+    @SdkSuppress(minSdkVersion = 31) // Older APIs does not support getSystemBarsBehavior
+    fun systemBarsBehavior() {
         scenario.onActivity {
             windowInsetsController.systemBarsBehavior =
-                WindowInsetsControllerCompat.BEHAVIOR_SHOW_BARS_BY_SWIPE
+                WindowInsetsControllerCompat.BEHAVIOR_DEFAULT
+            assertEquals(
+                WindowInsetsControllerCompat.BEHAVIOR_DEFAULT,
+                windowInsetsController.systemBarsBehavior
+            )
+            windowInsetsController.systemBarsBehavior =
+                WindowInsetsControllerCompat.BEHAVIOR_SHOW_TRANSIENT_BARS_BY_SWIPE
+            assertEquals(
+                WindowInsetsControllerCompat.BEHAVIOR_SHOW_TRANSIENT_BARS_BY_SWIPE,
+                windowInsetsController.systemBarsBehavior
+            )
+        }
+    }
+
+    @Test
+    // minSdkVersion = 21 due to b/189492236
+    @SdkSuppress(minSdkVersion = 21, maxSdkVersion = 29) // Flag deprecated in 30+
+    public fun systemBarsBehavior_default() {
+        scenario.onActivity {
+            windowInsetsController.systemBarsBehavior =
+                WindowInsetsControllerCompat.BEHAVIOR_DEFAULT
         }
         val decorView = scenario.withActivity { window.decorView }
         val sysUiVis = decorView.systemUiVisibility
@@ -409,23 +428,6 @@
         assertEquals(0, sysUiVis and View.SYSTEM_UI_FLAG_IMMERSIVE)
     }
 
-    @Test
-    public fun systemBarsBehavior_touch() {
-        scenario.onActivity {
-            windowInsetsController.systemBarsBehavior =
-                WindowInsetsControllerCompat.BEHAVIOR_SHOW_BARS_BY_TOUCH
-        }
-        val decorView = scenario.withActivity { window.decorView }
-        val sysUiVis = decorView.systemUiVisibility
-        assertEquals(
-            0,
-            sysUiVis and (
-                View.SYSTEM_UI_FLAG_IMMERSIVE
-                    or View.SYSTEM_UI_FLAG_IMMERSIVE_STICKY
-                )
-        )
-    }
-
     private fun assumeNotCuttlefish() {
         // TODO: remove this if b/159103848 is resolved
         assumeFalse(
diff --git a/core/core/src/androidTest/java/androidx/core/widget/NestedScrollViewWithCollapsingToolbarTest.java b/core/core/src/androidTest/java/androidx/core/widget/NestedScrollViewWithCollapsingToolbarTest.java
index 47133e0..74a1306 100644
--- a/core/core/src/androidTest/java/androidx/core/widget/NestedScrollViewWithCollapsingToolbarTest.java
+++ b/core/core/src/androidTest/java/androidx/core/widget/NestedScrollViewWithCollapsingToolbarTest.java
@@ -21,16 +21,16 @@
 import android.content.Context;
 import android.util.AttributeSet;
 import android.view.InputDevice;
+import android.view.KeyEvent;
 import android.view.MotionEvent;
 import android.view.View;
 import android.widget.FrameLayout;
 import android.widget.LinearLayout;
 import android.widget.TextView;
 
-import androidx.annotation.NonNull;
 import androidx.test.core.app.ApplicationProvider;
 import androidx.test.ext.junit.runners.AndroidJUnit4;
-import androidx.test.filters.SmallTest;
+import androidx.test.filters.MediumTest;
 
 import org.junit.Test;
 import org.junit.runner.RunWith;
@@ -39,7 +39,7 @@
  * Tests that CollapsingToolbarLayout properly collapses/expands with a NestedScrollView.
  */
 @RunWith(AndroidJUnit4.class)
-@SmallTest
+@MediumTest
 public class NestedScrollViewWithCollapsingToolbarTest {
     private static final String LONG_TEXT = "This is some long text. It just keeps going. Look at"
             + " it. Scroll it. Scroll the nested version of it. This is some long text. It just"
@@ -72,8 +72,9 @@
 
     private MockCoordinatorLayoutWithCollapsingToolbarAndNestedScrollView mChildNestedScrollView;
 
+    /*** Touch swiping tests at the top/bottom of the child ***/
     @Test
-    public void isOnStartNestedScrollTriggered_touchSwipeUpInChild_triggeredInParent() {
+    public void isOnStartNestedScrollCalled_touchSwipeUpInChild_calledInParent() {
         // Arrange
         setupNestedScrollViewInNestedScrollView(
                 ApplicationProvider.getApplicationContext(),
@@ -92,7 +93,7 @@
     }
 
     @Test
-    public void isOnStartNestedScrollTriggered_touchSwipeDownInChild_triggeredInParent() {
+    public void isOnStartNestedScrollCalled_touchSwipeDownInChild_calledInParent() {
         // Arrange
         setupNestedScrollViewInNestedScrollView(
                 ApplicationProvider.getApplicationContext(),
@@ -110,9 +111,9 @@
         assertEquals(0, mChildNestedScrollView.getOnStartNestedScrollCount());
     }
 
-
+    /*** Rotary scrolling tests at the top/bottom of the child ***/
     @Test
-    public void isOnStartNestedScrollTriggered_rotaryScrollInChildPastTop_triggeredInParent() {
+    public void isOnStartNestedScrollCalled_rotaryScrollInChildPastTop_calledInParent() {
         // Arrange
         setupNestedScrollViewInNestedScrollView(
                 ApplicationProvider.getApplicationContext(),
@@ -134,7 +135,7 @@
     }
 
     @Test
-    public void isOnStartNestedScrollTriggered_rotaryScrollInChildPastBottom_triggeredInParent() {
+    public void isOnStartNestedScrollCalled_rotaryScrollInChildPastBottom_calledInParent() {
         // Arrange
         setupNestedScrollViewInNestedScrollView(
                 ApplicationProvider.getApplicationContext(),
@@ -158,8 +159,9 @@
         assertEquals(0, mChildNestedScrollView.getOnStartNestedScrollCount());
     }
 
+    /*** Mouse scrolling tests at the top/bottom of the child ***/
     @Test
-    public void isOnStartNestedScrollTriggered_mouseScrollInChildPastTop_triggeredInParent() {
+    public void isOnStartNestedScrollCalled_mouseScrollInChildPastTop_calledInParent() {
         // Arrange
         setupNestedScrollViewInNestedScrollView(
                 ApplicationProvider.getApplicationContext(),
@@ -181,7 +183,7 @@
     }
 
     @Test
-    public void isOnStartNestedScrollTriggered_mouseScrollInChildPastBottom_triggeredInParent() {
+    public void isOnStartNestedScrollCalled_mouseScrollInChildPastBottom_calledInParent() {
         // Arrange
         setupNestedScrollViewInNestedScrollView(
                 ApplicationProvider.getApplicationContext(),
@@ -205,6 +207,319 @@
         assertEquals(0, mChildNestedScrollView.getOnStartNestedScrollCount());
     }
 
+    /*** Keyboard event tests BOTH inside the child and at the top/bottom of the child ***/
+    // Keyboard events within the child (should trigger OnStartNestedScroll() in parent)
+    @Test
+    public void isOnStartNestedScrollCalled_keyboardUpInChild_calledInParent() {
+        // Arrange
+        setupNestedScrollViewInNestedScrollView(
+                ApplicationProvider.getApplicationContext(),
+                100,
+                600);
+
+        // Move to bottom of the child NestedScrollView, so we can scroll up and not go past child.
+        int scrollRange = mChildNestedScrollView.getScrollRange();
+        mChildNestedScrollView.scrollTo(0, scrollRange);
+
+        // Act
+        mChildNestedScrollView.requestFocus();
+        KeyEvent keyEventPressDown = new KeyEvent(
+                0,
+                0,
+                KeyEvent.ACTION_DOWN,
+                KeyEvent.KEYCODE_DPAD_UP,
+                0);
+        mChildNestedScrollView.executeKeyEvent(keyEventPressDown);
+
+        KeyEvent keyEventPressUp = new KeyEvent(
+                0,
+                0,
+                KeyEvent.ACTION_UP,
+                KeyEvent.KEYCODE_DPAD_UP,
+                0);
+        mChildNestedScrollView.executeKeyEvent(keyEventPressUp);
+
+        // Assert
+        // Should trigger a scroll event in parent. Note: OnStartNestedScroll is triggered on
+        // key action down only, not key action up, so that is why the count is one.
+        assertEquals(1, mParentNestedScrollView.getOnStartNestedScrollCount());
+        // Should not trigger in child (because child doesn't have its own inner NestedScrollView).
+        assertEquals(0, mChildNestedScrollView.getOnStartNestedScrollCount());
+    }
+
+
+    @Test
+    public void isOnStartNestedScrollCalled_keyboardDownInChild_calledInParent() {
+        // Arrange
+        setupNestedScrollViewInNestedScrollView(
+                ApplicationProvider.getApplicationContext(),
+                100,
+                600);
+
+        // Act
+        mChildNestedScrollView.requestFocus();
+        KeyEvent keyEventPressDown = new KeyEvent(
+                0,
+                0,
+                KeyEvent.ACTION_DOWN,
+                KeyEvent.KEYCODE_DPAD_DOWN,
+                0);
+        mChildNestedScrollView.executeKeyEvent(keyEventPressDown);
+
+        KeyEvent keyEventPressUp = new KeyEvent(
+                0,
+                0,
+                KeyEvent.ACTION_UP,
+                KeyEvent.KEYCODE_DPAD_DOWN,
+                0);
+        mChildNestedScrollView.executeKeyEvent(keyEventPressUp);
+
+        // Assert
+        // Should trigger a scroll event in parent. Note: OnStartNestedScroll is triggered on
+        // key action down only, not key action up, so that is why the count is one.
+        // Should trigger in parent of scroll event.
+        assertEquals(1, mParentNestedScrollView.getOnStartNestedScrollCount());
+        // Should not trigger in child (because child doesn't have its own inner NestedScrollView).
+        assertEquals(0, mChildNestedScrollView.getOnStartNestedScrollCount());
+    }
+
+    // Keyboard events at the top/bottom bounds of the child (should NOT trigger
+    // OnStartNestedScroll() in the parent).
+
+    // For events at the bounds of the nested child, Keyboard events are handled a little different
+    // from the rest. If they are at the bound, they will not handle the event
+    // (return false) and so the container view will handle it (something like CoordinatorLayout).
+    // Where with the other types (from Touch, Rotary, and Scroll), the NestedScrollView will
+    // handle those bound crossing events itself, and thus why these tests don't have a
+    // OnStartNestedScroll() in the parent
+
+    // Keyboard events inside the child (should still trigger OnStartNestedScroll() in parent)
+    @Test
+    public void isOnStartNestedScrollCalled_keyboardUpInChildPastTop_notCalledInParent() {
+        // Arrange
+        setupNestedScrollViewInNestedScrollView(
+                ApplicationProvider.getApplicationContext(),
+                100,
+                600);
+
+        // Act
+        mChildNestedScrollView.requestFocus();
+        KeyEvent keyEventPressDown = new KeyEvent(
+                0,
+                0,
+                KeyEvent.ACTION_DOWN,
+                KeyEvent.KEYCODE_DPAD_UP,
+                0);
+        mChildNestedScrollView.executeKeyEvent(keyEventPressDown);
+
+        KeyEvent keyEventPressUp = new KeyEvent(
+                0,
+                0,
+                KeyEvent.ACTION_UP,
+                KeyEvent.KEYCODE_DPAD_UP,
+                0);
+        mChildNestedScrollView.executeKeyEvent(keyEventPressUp);
+
+        // Assert
+        // Should trigger in parent of scroll event. Note: OnStartNestedScroll is triggered on
+        // key action down only, not key action up, so that is why the count is one.
+        assertEquals(0, mParentNestedScrollView.getOnStartNestedScrollCount());
+        // Should not trigger in child (because child doesn't have its own inner NestedScrollView).
+        assertEquals(0, mChildNestedScrollView.getOnStartNestedScrollCount());
+    }
+
+    @Test
+    public void isOnStartNestedScrollCalled_keyboardDownInChildPastBottom_notCalledInParent() {
+        // Arrange
+        setupNestedScrollViewInNestedScrollView(
+                ApplicationProvider.getApplicationContext(),
+                100,
+                600);
+        // Move to bottom of the child NestedScrollView, so we can try scrolling past it.
+        int scrollRange = mChildNestedScrollView.getScrollRange();
+        mChildNestedScrollView.scrollTo(0, scrollRange);
+
+        // Act
+        mChildNestedScrollView.requestFocus();
+        KeyEvent keyEventPressDown = new KeyEvent(
+                0,
+                0,
+                KeyEvent.ACTION_DOWN,
+                KeyEvent.KEYCODE_DPAD_DOWN,
+                0);
+        mChildNestedScrollView.executeKeyEvent(keyEventPressDown);
+
+        KeyEvent keyEventPressUp = new KeyEvent(
+                0,
+                0,
+                KeyEvent.ACTION_UP,
+                KeyEvent.KEYCODE_DPAD_DOWN,
+                0);
+        mChildNestedScrollView.executeKeyEvent(keyEventPressUp);
+
+        // Assert
+        // Should trigger in parent of scroll event. Note: OnStartNestedScroll is triggered on
+        // key action down only, not key action up, so that is why the count is one.
+        // Should trigger in parent of scroll event.
+        assertEquals(0, mParentNestedScrollView.getOnStartNestedScrollCount());
+        // Should not trigger in child (because child doesn't have its own inner NestedScrollView).
+        assertEquals(0, mChildNestedScrollView.getOnStartNestedScrollCount());
+    }
+
+    @Test
+    public void isOnStartNestedScrollCalled_keyboardAltUpInChildPastTop_notCalledInParent() {
+        // Arrange
+        setupNestedScrollViewInNestedScrollView(
+                ApplicationProvider.getApplicationContext(),
+                100,
+                600);
+
+        // Act
+        mChildNestedScrollView.requestFocus();
+        KeyEvent keyEventPressDown = new KeyEvent(
+                0,
+                0,
+                KeyEvent.ACTION_DOWN,
+                KeyEvent.KEYCODE_DPAD_UP,
+                0,
+                KeyEvent.META_ALT_ON | KeyEvent.META_ALT_LEFT_ON
+        );
+        mChildNestedScrollView.executeKeyEvent(keyEventPressDown);
+
+        KeyEvent keyEventPressUp = new KeyEvent(
+                0,
+                0,
+                KeyEvent.ACTION_UP,
+                KeyEvent.KEYCODE_DPAD_UP,
+                0,
+                KeyEvent.META_ALT_ON | KeyEvent.META_ALT_LEFT_ON
+        );
+        mChildNestedScrollView.executeKeyEvent(keyEventPressUp);
+
+        // Assert
+        // Should trigger in parent of scroll event. Note: OnStartNestedScroll is triggered on
+        // key action down only, not key action up, so that is why the count is one.
+        // Should trigger in parent of scroll event.
+        assertEquals(0, mParentNestedScrollView.getOnStartNestedScrollCount());
+        // Should not trigger in child (because child doesn't have its own inner NestedScrollView).
+        assertEquals(0, mChildNestedScrollView.getOnStartNestedScrollCount());
+    }
+
+    @Test
+    public void isOnStartNestedScrollCalled_keyboardAltDownInChildPastBottom_notCalledInParent() {
+        // Arrange
+        setupNestedScrollViewInNestedScrollView(
+                ApplicationProvider.getApplicationContext(),
+                100,
+                600);
+        // Move to bottom of the child NestedScrollView, so we can try scrolling past it.
+        int scrollRange = mChildNestedScrollView.getScrollRange();
+        mChildNestedScrollView.scrollTo(0, scrollRange);
+
+        mChildNestedScrollView.requestFocus();
+        KeyEvent keyEventPressDown = new KeyEvent(
+                0,
+                0,
+                KeyEvent.ACTION_DOWN,
+                KeyEvent.KEYCODE_DPAD_DOWN,
+                0,
+                KeyEvent.META_ALT_ON | KeyEvent.META_ALT_LEFT_ON
+        );
+        mChildNestedScrollView.executeKeyEvent(keyEventPressDown);
+
+        KeyEvent keyEventPressUp = new KeyEvent(
+                0,
+                0,
+                KeyEvent.ACTION_UP,
+                KeyEvent.KEYCODE_DPAD_DOWN,
+                0,
+                KeyEvent.META_ALT_ON | KeyEvent.META_ALT_LEFT_ON
+        );
+        mChildNestedScrollView.executeKeyEvent(keyEventPressUp);
+
+        // Assert
+        // Should trigger in parent of scroll event. Note: OnStartNestedScroll is triggered on
+        // key action down only, not key action up, so that is why the count is one.
+        // Should trigger in parent of scroll event.
+        assertEquals(0, mParentNestedScrollView.getOnStartNestedScrollCount());
+        // Should not trigger in child (because child doesn't have its own inner NestedScrollView).
+        assertEquals(0, mChildNestedScrollView.getOnStartNestedScrollCount());
+    }
+
+    @Test
+    public void isOnStartNestedScrollCalled_keyboardShiftSpaceInChildPastTop_notCalledInParent() {
+        // Arrange
+        setupNestedScrollViewInNestedScrollView(
+                ApplicationProvider.getApplicationContext(),
+                100,
+                600);
+
+        // Act
+        mChildNestedScrollView.requestFocus();
+        KeyEvent keyEventPressDown = new KeyEvent(
+                0,
+                0,
+                KeyEvent.ACTION_DOWN,
+                KeyEvent.KEYCODE_SPACE,
+                0,
+                KeyEvent.META_SHIFT_ON);
+        mChildNestedScrollView.executeKeyEvent(keyEventPressDown);
+
+        KeyEvent keyEventPressUp = new KeyEvent(
+                0,
+                0,
+                KeyEvent.ACTION_UP,
+                KeyEvent.KEYCODE_SPACE,
+                0,
+                KeyEvent.META_SHIFT_ON);
+        mChildNestedScrollView.executeKeyEvent(keyEventPressUp);
+
+        // Assert
+        // Should trigger in parent of scroll event. Note: OnStartNestedScroll is triggered on
+        // key action down only, not key action up, so that is why the count is one.
+        // Should trigger in parent of scroll event.
+        assertEquals(0, mParentNestedScrollView.getOnStartNestedScrollCount());
+        // Should not trigger in child (because child doesn't have its own inner NestedScrollView).
+        assertEquals(0, mChildNestedScrollView.getOnStartNestedScrollCount());
+    }
+
+    @Test
+    public void isOnStartNestedScrollCalled_keyboardSpaceInChildPastBottom_notCalledInParent() {
+        // Arrange
+        setupNestedScrollViewInNestedScrollView(
+                ApplicationProvider.getApplicationContext(),
+                100,
+                600);
+        // Move to bottom of the child NestedScrollView, so we can try scrolling past it.
+        int scrollRange = mChildNestedScrollView.getScrollRange();
+        mChildNestedScrollView.scrollTo(0, scrollRange);
+
+        // Act
+        mChildNestedScrollView.requestFocus();
+        KeyEvent keyEventPressDown = new KeyEvent(
+                0,
+                0,
+                KeyEvent.ACTION_DOWN,
+                KeyEvent.KEYCODE_SPACE,
+                0);
+        mChildNestedScrollView.executeKeyEvent(keyEventPressDown);
+
+        KeyEvent keyEventPressUp = new KeyEvent(
+                0,
+                0,
+                KeyEvent.ACTION_UP,
+                KeyEvent.KEYCODE_SPACE,
+                0);
+        mChildNestedScrollView.executeKeyEvent(keyEventPressUp);
+
+        // Assert
+        // Should trigger in parent of scroll event. Note: OnStartNestedScroll is triggered on
+        // key action down only, not key action up, so that is why the count is one.
+        // Should trigger in parent of scroll event.
+        assertEquals(0, mParentNestedScrollView.getOnStartNestedScrollCount());
+        // Should not trigger in child (because child doesn't have its own inner NestedScrollView).
+        assertEquals(0, mChildNestedScrollView.getOnStartNestedScrollCount());
+    }
 
     private TextView createTextView(Context context, int width, int height, String textContent) {
         TextView textView = new TextView(context);
@@ -221,10 +536,13 @@
     }
 
     private void setupNestedScrollViewInNestedScrollView(Context context, int width, int height) {
+
+        // 1. Setup Views
+
         // The parent NestedScrollView contains a LinearLayout with three Views:
-        //  1. TextView
-        //  2. A child NestedScrollView (contains its own TextView)
-        //  3. TextView
+        //  a. TextView
+        //  b. A child NestedScrollView (contains its own TextView)
+        //  c. TextView
         int childHeight = height / 3;
 
         // Creates child NestedScrollView first
@@ -264,6 +582,16 @@
         mParentNestedScrollView.setMinimumHeight(height);
         mParentNestedScrollView.setBackgroundColor(0xCC00FF00);
         mParentNestedScrollView.addView(linearLayout);
+
+        // 2. Measure Parent
+        int measureSpecWidth =
+                View.MeasureSpec.makeMeasureSpec(width, View.MeasureSpec.EXACTLY);
+        int measureSpecHeight =
+                View.MeasureSpec.makeMeasureSpec(height, View.MeasureSpec.EXACTLY);
+        mParentNestedScrollView.measure(measureSpecWidth, measureSpecHeight);
+
+        // 3. Layout Parent
+        mParentNestedScrollView.layout(0, 0, width, height);
     }
 
     private void swipeDown(View view, boolean shortSwipe) {
@@ -373,12 +701,7 @@
         }
 
         @Override
-        public boolean onStartNestedScroll(
-                @NonNull View child,
-                @NonNull View target,
-                int axes,
-                int type
-        ) {
+        public boolean onStartNestedScroll(View child, View target, int axes, int type) {
             mOnStartNestedScrollCount++;
             return super.onStartNestedScroll(child, target, axes, type);
         }
diff --git a/core/core/src/main/java/androidx/core/view/WindowInsetsControllerCompat.java b/core/core/src/main/java/androidx/core/view/WindowInsetsControllerCompat.java
index 9a49066..bff7344 100644
--- a/core/core/src/main/java/androidx/core/view/WindowInsetsControllerCompat.java
+++ b/core/core/src/main/java/androidx/core/view/WindowInsetsControllerCompat.java
@@ -54,22 +54,43 @@
 public final class WindowInsetsControllerCompat {
 
     /**
-     * The default option for {@link #setSystemBarsBehavior(int)}. System bars will be forcibly
-     * shown on any user interaction on the corresponding display if navigation bars are hidden
-     * by {@link #hide(int)} or
+     * Option for {@link #setSystemBarsBehavior(int)}. System bars will be forcibly shown on any
+     * user interaction on the corresponding display if navigation bars are hidden by
+     * {@link #hide(int)} or
      * {@link WindowInsetsAnimationControllerCompat#setInsetsAndAlpha(Insets, float, float)}.
+     *
+     * @deprecated This is not supported on Android {@link android.os.Build.VERSION_CODES#S} and
+     * later. Use {@link #BEHAVIOR_DEFAULT} or {@link #BEHAVIOR_SHOW_TRANSIENT_BARS_BY_SWIPE}
+     * instead.
      */
+    @Deprecated
     public static final int BEHAVIOR_SHOW_BARS_BY_TOUCH = 0;
 
     /**
+     * The default option for {@link #setSystemBarsBehavior(int)}: Window would like to remain
+     * interactive when hiding navigation bars by calling {@link #hide(int)} or
+     * {@link WindowInsetsAnimationControllerCompat#setInsetsAndAlpha(Insets, float, float)}.
+     *
+     * <p>When system bars are hidden in this mode, they can be revealed with system gestures, such
+     * as swiping from the edge of the screen where the bar is hidden from.</p>
+     *
+     * <p>When the gesture navigation is enabled, the system gestures can be triggered regardless
+     * the visibility of system bars.</p>
+     */
+    public static final int BEHAVIOR_DEFAULT = 1;
+
+    /**
      * Option for {@link #setSystemBarsBehavior(int)}: Window would like to remain interactive
      * when hiding navigation bars by calling {@link #hide(int)} or
      * {@link WindowInsetsAnimationControllerCompat#setInsetsAndAlpha(Insets, float, float)}.
      * <p>
      * When system bars are hidden in this mode, they can be revealed with system
      * gestures, such as swiping from the edge of the screen where the bar is hidden from.
+     *
+     * @deprecated Use {@link #BEHAVIOR_DEFAULT} instead.
      */
-    public static final int BEHAVIOR_SHOW_BARS_BY_SWIPE = 1;
+    @Deprecated
+    public static final int BEHAVIOR_SHOW_BARS_BY_SWIPE = BEHAVIOR_DEFAULT;
 
     /**
      * Option for {@link #setSystemBarsBehavior(int)}: Window would like to remain
@@ -135,8 +156,7 @@
      */
     @RestrictTo(RestrictTo.Scope.LIBRARY)
     @Retention(RetentionPolicy.SOURCE)
-    @IntDef(value = {BEHAVIOR_SHOW_BARS_BY_TOUCH, BEHAVIOR_SHOW_BARS_BY_SWIPE,
-            BEHAVIOR_SHOW_TRANSIENT_BARS_BY_SWIPE})
+    @IntDef(value = {BEHAVIOR_DEFAULT, BEHAVIOR_SHOW_TRANSIENT_BARS_BY_SWIPE})
     @interface Behavior {
     }
 
@@ -515,7 +535,7 @@
         @Override
         void setSystemBarsBehavior(int behavior) {
             switch (behavior) {
-                case BEHAVIOR_SHOW_BARS_BY_SWIPE:
+                case BEHAVIOR_DEFAULT:
                     unsetSystemUiFlag(View.SYSTEM_UI_FLAG_IMMERSIVE_STICKY);
                     setSystemUiFlag(View.SYSTEM_UI_FLAG_IMMERSIVE);
                     break;
diff --git a/core/core/src/main/java/androidx/core/widget/NestedScrollView.java b/core/core/src/main/java/androidx/core/widget/NestedScrollView.java
index 24b6a4b..279e003 100644
--- a/core/core/src/main/java/androidx/core/widget/NestedScrollView.java
+++ b/core/core/src/main/java/androidx/core/widget/NestedScrollView.java
@@ -1034,17 +1034,19 @@
 
     /*
      * Handles scroll events for both touch and non-touch events (mouse scroll wheel,
-     * rotary button, etc.).
+     * rotary button, keyboard, etc.).
      *
-     * Note: This returns the total scroll offset for touch event which is required for calculating
-     * the scroll between move events. This returned value is NOT needed for non-touch events since
-     * a scroll is a one time event.
+     * Note: This function returns the total scroll offset for this scroll event which is required
+     * for calculating the total scroll between multiple move events (touch). This returned value
+     * is NOT needed for non-touch events since a scroll is a one time event (vs. touch where a
+     * drag may be triggered multiple times with the movement of the finger).
      */
+    // TODO: You should rename this to nestedScrollBy() so it is different from View.scrollBy
     private int scrollBy(
             int verticalScrollDistance,
             int x,
             int touchType,
-            boolean isSourceMouse
+            boolean isSourceMouseOrKeyboard
     ) {
         int totalScrollOffset = 0;
 
@@ -1081,7 +1083,7 @@
 
         // Overscroll is for adding animations at the top/bottom of a view when the user scrolls
         // beyond the beginning/end of the view. Overscroll is not used with a mouse.
-        boolean canOverscroll = canOverScroll() && !isSourceMouse;
+        boolean canOverscroll = canOverScroll() && !isSourceMouseOrKeyboard;
 
         // Scrolls content in the current View, but clamps it if it goes too far.
         boolean hitScrollBarrier =
@@ -1585,7 +1587,6 @@
                 mTempRect.top = mTempRect.bottom - height;
             }
         }
-
         return scrollAndFocus(direction, mTempRect.top, mTempRect.bottom);
     }
 
@@ -1618,7 +1619,7 @@
             handled = false;
         } else {
             int delta = up ? (top - containerTop) : (bottom - containerBottom);
-            doScrollY(delta);
+            scrollBy(delta, 0, ViewCompat.TYPE_NON_TOUCH, true);
         }
 
         if (newFocused != findFocus()) newFocused.requestFocus(direction);
@@ -1645,8 +1646,10 @@
             nextFocused.getDrawingRect(mTempRect);
             offsetDescendantRectToMyCoords(nextFocused, mTempRect);
             int scrollDelta = computeScrollDeltaToGetChildRectOnScreen(mTempRect);
-            doScrollY(scrollDelta);
+
+            scrollBy(scrollDelta, 0, ViewCompat.TYPE_NON_TOUCH, true);
             nextFocused.requestFocus(direction);
+
         } else {
             // no new focus
             int scrollDelta = maxJump;
@@ -1665,7 +1668,9 @@
             if (scrollDelta == 0) {
                 return false;
             }
-            doScrollY(direction == View.FOCUS_DOWN ? scrollDelta : -scrollDelta);
+
+            int finalScrollDelta = direction == View.FOCUS_DOWN ? scrollDelta : -scrollDelta;
+            scrollBy(finalScrollDelta, 0, ViewCompat.TYPE_NON_TOUCH, true);
         }
 
         if (currentFocused != null && currentFocused.isFocused()
diff --git a/docs-public/build.gradle b/docs-public/build.gradle
index ef767e5..429d083 100644
--- a/docs-public/build.gradle
+++ b/docs-public/build.gradle
@@ -8,13 +8,13 @@
 }
 
 dependencies {
-    docs("androidx.activity:activity:1.7.0-alpha03")
-    docs("androidx.activity:activity-compose:1.7.0-alpha03")
-    samples("androidx.activity:activity-compose-samples:1.7.0-alpha03")
-    docs("androidx.activity:activity-ktx:1.7.0-alpha03")
+    docs("androidx.activity:activity:1.7.0-alpha04")
+    docs("androidx.activity:activity-compose:1.7.0-alpha04")
+    samples("androidx.activity:activity-compose-samples:1.7.0-alpha04")
+    docs("androidx.activity:activity-ktx:1.7.0-alpha04")
     docs("androidx.ads:ads-identifier:1.0.0-alpha04")
     docs("androidx.ads:ads-identifier-provider:1.0.0-alpha04")
-    docs("androidx.annotation:annotation:1.6.0-alpha01")
+    docs("androidx.annotation:annotation:1.6.0-alpha02")
     docs("androidx.annotation:annotation-experimental:1.3.0")
     docs("androidx.appcompat:appcompat:1.7.0-alpha01")
     docs("androidx.appcompat:appcompat-resources:1.7.0-alpha01")
@@ -23,9 +23,9 @@
     docs("androidx.appsearch:appsearch-ktx:1.1.0-alpha02")
     docs("androidx.appsearch:appsearch-local-storage:1.1.0-alpha02")
     docs("androidx.appsearch:appsearch-platform-storage:1.1.0-alpha02")
-    docs("androidx.arch.core:core-common:2.2.0-alpha01")
-    docs("androidx.arch.core:core-runtime:2.2.0-alpha01")
-    docs("androidx.arch.core:core-testing:2.2.0-alpha01")
+    docs("androidx.arch.core:core-common:2.2.0-beta01")
+    docs("androidx.arch.core:core-runtime:2.2.0-beta01")
+    docs("androidx.arch.core:core-testing:2.2.0-beta01")
     docs("androidx.asynclayoutinflater:asynclayoutinflater:1.1.0-alpha01")
     docs("androidx.asynclayoutinflater:asynclayoutinflater-appcompat:1.1.0-alpha01")
     docs("androidx.autofill:autofill:1.2.0-beta01")
@@ -36,17 +36,17 @@
     docs("androidx.biometric:biometric:1.2.0-alpha05")
     docs("androidx.biometric:biometric-ktx:1.2.0-alpha05")
     samples("androidx.biometric:biometric-ktx-samples:1.2.0-alpha05")
-    docs("androidx.browser:browser:1.5.0-beta01")
-    docs("androidx.camera:camera-camera2:1.3.0-alpha02")
-    docs("androidx.camera:camera-core:1.3.0-alpha02")
-    docs("androidx.camera:camera-extensions:1.3.0-alpha02")
+    docs("androidx.browser:browser:1.5.0-rc01")
+    docs("androidx.camera:camera-camera2:1.3.0-alpha03")
+    docs("androidx.camera:camera-core:1.3.0-alpha03")
+    docs("androidx.camera:camera-extensions:1.3.0-alpha03")
     stubs(fileTree(dir: "../camera/camera-extensions-stub", include: ["camera-extensions-stub.jar"]))
-    docs("androidx.camera:camera-lifecycle:1.3.0-alpha02")
-    docs("androidx.camera:camera-mlkit-vision:1.3.0-alpha02")
+    docs("androidx.camera:camera-lifecycle:1.3.0-alpha03")
+    docs("androidx.camera:camera-mlkit-vision:1.3.0-alpha03")
     docs("androidx.camera:camera-previewview:1.1.0-beta02")
-    docs("androidx.camera:camera-video:1.3.0-alpha02")
-    docs("androidx.camera:camera-view:1.3.0-alpha02")
-    docs("androidx.camera:camera-viewfinder:1.3.0-alpha02")
+    docs("androidx.camera:camera-video:1.3.0-alpha03")
+    docs("androidx.camera:camera-view:1.3.0-alpha03")
+    docs("androidx.camera:camera-viewfinder:1.3.0-alpha03")
     docs("androidx.car.app:app:1.3.0-rc01")
     docs("androidx.car.app:app-automotive:1.3.0-rc01")
     docs("androidx.car.app:app-projected:1.3.0-rc01")
@@ -54,60 +54,60 @@
     docs("androidx.cardview:cardview:1.0.0")
     docs("androidx.collection:collection:1.3.0-alpha02")
     docs("androidx.collection:collection-ktx:1.3.0-alpha02")
-    docs("androidx.compose.animation:animation:1.4.0-alpha04")
-    docs("androidx.compose.animation:animation-core:1.4.0-alpha04")
-    docs("androidx.compose.animation:animation-graphics:1.4.0-alpha04")
-    samples("androidx.compose.animation:animation-samples:1.4.0-alpha04")
-    samples("androidx.compose.animation:animation-core-samples:1.4.0-alpha04")
-    samples("androidx.compose.animation:animation-graphics-samples:1.4.0-alpha04")
-    docs("androidx.compose.foundation:foundation:1.4.0-alpha04")
-    docs("androidx.compose.foundation:foundation-layout:1.4.0-alpha04")
-    samples("androidx.compose.foundation:foundation-layout-samples:1.4.0-alpha04")
-    samples("androidx.compose.foundation:foundation-samples:1.4.0-alpha04")
+    docs("androidx.compose.animation:animation:1.4.0-alpha05")
+    docs("androidx.compose.animation:animation-core:1.4.0-alpha05")
+    docs("androidx.compose.animation:animation-graphics:1.4.0-alpha05")
+    samples("androidx.compose.animation:animation-samples:1.4.0-alpha05")
+    samples("androidx.compose.animation:animation-core-samples:1.4.0-alpha05")
+    samples("androidx.compose.animation:animation-graphics-samples:1.4.0-alpha05")
+    docs("androidx.compose.foundation:foundation:1.4.0-alpha05")
+    docs("androidx.compose.foundation:foundation-layout:1.4.0-alpha05")
+    samples("androidx.compose.foundation:foundation-layout-samples:1.4.0-alpha05")
+    samples("androidx.compose.foundation:foundation-samples:1.4.0-alpha05")
     docs("androidx.compose.material3:material3:1.1.0-alpha04")
     samples("androidx.compose.material3:material3-samples:1.1.0-alpha04")
     docs("androidx.compose.material3:material3-window-size-class:1.1.0-alpha04")
     samples("androidx.compose.material3:material3-window-size-class-samples:1.1.0-alpha04")
-    docs("androidx.compose.material:material:1.4.0-alpha04")
-    docs("androidx.compose.material:material-icons-core:1.4.0-alpha04")
-    samples("androidx.compose.material:material-icons-core-samples:1.4.0-alpha04")
-    docs("androidx.compose.material:material-ripple:1.4.0-alpha04")
-    samples("androidx.compose.material:material-samples:1.4.0-alpha04")
-    docs("androidx.compose.runtime:runtime:1.4.0-alpha04")
-    docs("androidx.compose.runtime:runtime-livedata:1.4.0-alpha04")
-    samples("androidx.compose.runtime:runtime-livedata-samples:1.4.0-alpha04")
-    docs("androidx.compose.runtime:runtime-rxjava2:1.4.0-alpha04")
-    samples("androidx.compose.runtime:runtime-rxjava2-samples:1.4.0-alpha04")
-    docs("androidx.compose.runtime:runtime-rxjava3:1.4.0-alpha04")
-    samples("androidx.compose.runtime:runtime-rxjava3-samples:1.4.0-alpha04")
-    docs("androidx.compose.runtime:runtime-saveable:1.4.0-alpha04")
-    samples("androidx.compose.runtime:runtime-saveable-samples:1.4.0-alpha04")
-    samples("androidx.compose.runtime:runtime-samples:1.4.0-alpha04")
+    docs("androidx.compose.material:material:1.4.0-alpha05")
+    docs("androidx.compose.material:material-icons-core:1.4.0-alpha05")
+    samples("androidx.compose.material:material-icons-core-samples:1.4.0-alpha05")
+    docs("androidx.compose.material:material-ripple:1.4.0-alpha05")
+    samples("androidx.compose.material:material-samples:1.4.0-alpha05")
+    docs("androidx.compose.runtime:runtime:1.4.0-alpha05")
+    docs("androidx.compose.runtime:runtime-livedata:1.4.0-alpha05")
+    samples("androidx.compose.runtime:runtime-livedata-samples:1.4.0-alpha05")
+    docs("androidx.compose.runtime:runtime-rxjava2:1.4.0-alpha05")
+    samples("androidx.compose.runtime:runtime-rxjava2-samples:1.4.0-alpha05")
+    docs("androidx.compose.runtime:runtime-rxjava3:1.4.0-alpha05")
+    samples("androidx.compose.runtime:runtime-rxjava3-samples:1.4.0-alpha05")
+    docs("androidx.compose.runtime:runtime-saveable:1.4.0-alpha05")
+    samples("androidx.compose.runtime:runtime-saveable-samples:1.4.0-alpha05")
+    samples("androidx.compose.runtime:runtime-samples:1.4.0-alpha05")
     docs("androidx.compose.runtime:runtime-tracing:1.0.0-alpha02")
-    docs("androidx.compose.ui:ui:1.4.0-alpha04")
-    docs("androidx.compose.ui:ui-geometry:1.4.0-alpha04")
-    docs("androidx.compose.ui:ui-graphics:1.4.0-alpha04")
-    samples("androidx.compose.ui:ui-graphics-samples:1.4.0-alpha04")
-    docs("androidx.compose.ui:ui-test:1.4.0-alpha04")
-    docs("androidx.compose.ui:ui-test-junit4:1.4.0-alpha04")
-    samples("androidx.compose.ui:ui-test-samples:1.4.0-alpha04")
-    docs("androidx.compose.ui:ui-text:1.4.0-alpha04")
-    docs("androidx.compose.ui:ui-text-google-fonts:1.4.0-alpha04")
-    samples("androidx.compose.ui:ui-text-samples:1.4.0-alpha04")
-    docs("androidx.compose.ui:ui-tooling:1.4.0-alpha04")
-    docs("androidx.compose.ui:ui-tooling-data:1.4.0-alpha04")
-    docs("androidx.compose.ui:ui-tooling-preview:1.4.0-alpha04")
-    docs("androidx.compose.ui:ui-unit:1.4.0-alpha04")
-    samples("androidx.compose.ui:ui-unit-samples:1.4.0-alpha04")
-    docs("androidx.compose.ui:ui-util:1.4.0-alpha04")
-    docs("androidx.compose.ui:ui-viewbinding:1.4.0-alpha04")
-    samples("androidx.compose.ui:ui-viewbinding-samples:1.4.0-alpha04")
-    samples("androidx.compose.ui:ui-samples:1.4.0-alpha04")
+    docs("androidx.compose.ui:ui:1.4.0-alpha05")
+    docs("androidx.compose.ui:ui-geometry:1.4.0-alpha05")
+    docs("androidx.compose.ui:ui-graphics:1.4.0-alpha05")
+    samples("androidx.compose.ui:ui-graphics-samples:1.4.0-alpha05")
+    docs("androidx.compose.ui:ui-test:1.4.0-alpha05")
+    docs("androidx.compose.ui:ui-test-junit4:1.4.0-alpha05")
+    samples("androidx.compose.ui:ui-test-samples:1.4.0-alpha05")
+    docs("androidx.compose.ui:ui-text:1.4.0-alpha05")
+    docs("androidx.compose.ui:ui-text-google-fonts:1.4.0-alpha05")
+    samples("androidx.compose.ui:ui-text-samples:1.4.0-alpha05")
+    docs("androidx.compose.ui:ui-tooling:1.4.0-alpha05")
+    docs("androidx.compose.ui:ui-tooling-data:1.4.0-alpha05")
+    docs("androidx.compose.ui:ui-tooling-preview:1.4.0-alpha05")
+    docs("androidx.compose.ui:ui-unit:1.4.0-alpha05")
+    samples("androidx.compose.ui:ui-unit-samples:1.4.0-alpha05")
+    docs("androidx.compose.ui:ui-util:1.4.0-alpha05")
+    docs("androidx.compose.ui:ui-viewbinding:1.4.0-alpha05")
+    samples("androidx.compose.ui:ui-viewbinding-samples:1.4.0-alpha05")
+    samples("androidx.compose.ui:ui-samples:1.4.0-alpha05")
     docs("androidx.concurrent:concurrent-futures:1.1.0")
     docs("androidx.concurrent:concurrent-futures-ktx:1.1.0")
-    docs("androidx.constraintlayout:constraintlayout:2.2.0-alpha05")
-    docs("androidx.constraintlayout:constraintlayout-compose:1.1.0-alpha05")
-    docs("androidx.constraintlayout:constraintlayout-core:1.1.0-alpha05")
+    docs("androidx.constraintlayout:constraintlayout:2.2.0-alpha06")
+    docs("androidx.constraintlayout:constraintlayout-compose:1.1.0-alpha06")
+    docs("androidx.constraintlayout:constraintlayout-core:1.1.0-alpha06")
     docs("androidx.contentpager:contentpager:1.0.0")
     docs("androidx.coordinatorlayout:coordinatorlayout:1.2.0")
     docs("androidx.core.uwb:uwb:1.0.0-alpha04")
@@ -119,8 +119,8 @@
     docs("androidx.core:core-role:1.2.0-alpha01")
     docs("androidx.core:core-animation:1.0.0-beta02")
     docs("androidx.core:core-animation-testing:1.0.0-beta01")
-    docs("androidx.core:core:1.10.0-alpha01")
-    docs("androidx.core:core-ktx:1.10.0-alpha01")
+    docs("androidx.core:core:1.10.0-alpha02")
+    docs("androidx.core:core-ktx:1.10.0-alpha02")
     docs("androidx.core:core-splashscreen:1.1.0-alpha01")
     docs("androidx.credentials:credentials:1.0.0-alpha01")
     docs("androidx.credentials:credentials-play-services-auth:1.0.0-alpha01")
@@ -141,10 +141,11 @@
     docs("androidx.drawerlayout:drawerlayout:1.2.0-alpha01")
     docs("androidx.dynamicanimation:dynamicanimation:1.1.0-alpha02")
     docs("androidx.dynamicanimation:dynamicanimation-ktx:1.0.0-alpha03")
-    docs("androidx.emoji2:emoji2:1.3.0-alpha01")
-    docs("androidx.emoji2:emoji2-bundled:1.3.0-alpha01")
-    docs("androidx.emoji2:emoji2-views:1.3.0-alpha01")
-    docs("androidx.emoji2:emoji2-views-helper:1.3.0-alpha01")
+    docs("androidx.emoji2:emoji2:1.3.0-beta01")
+    docs("androidx.emoji2:emoji2-bundled:1.3.0-beta01")
+    docs("androidx.emoji2:emoji2-emojipicker:1.0.0-alpha01")
+    docs("androidx.emoji2:emoji2-views:1.3.0-beta01")
+    docs("androidx.emoji2:emoji2-views-helper:1.3.0-beta01")
     docs("androidx.emoji:emoji:1.2.0-alpha03")
     docs("androidx.emoji:emoji-appcompat:1.2.0-alpha03")
     docs("androidx.emoji:emoji-bundled:1.2.0-alpha03")
@@ -163,8 +164,8 @@
     docs("androidx.glance:glance-wear-tiles-preview:1.0.0-alpha05")
     docs("androidx.graphics:graphics-core:1.0.0-alpha02")
     docs("androidx.gridlayout:gridlayout:1.0.0")
-    docs("androidx.health.connect:connect-client:1.0.0-alpha09")
-    samples("androidx.health.connect:connect-client-samples:1.0.0-alpha09")
+    docs("androidx.health.connect:connect-client:1.0.0-alpha10")
+    samples("androidx.health.connect:connect-client-samples:1.0.0-alpha10")
     docs("androidx.health:health-services-client:1.0.0-beta02")
     docs("androidx.heifwriter:heifwriter:1.1.0-alpha01")
     docs("androidx.hilt:hilt-common:1.0.0-beta01")
@@ -181,27 +182,27 @@
     docs("androidx.leanback:leanback-paging:1.1.0-alpha09")
     docs("androidx.leanback:leanback-preference:1.2.0-alpha02")
     docs("androidx.leanback:leanback-tab:1.1.0-beta01")
-    docs("androidx.lifecycle:lifecycle-common:2.6.0-alpha04")
-    docs("androidx.lifecycle:lifecycle-common-java8:2.6.0-alpha04")
+    docs("androidx.lifecycle:lifecycle-common:2.6.0-alpha05")
+    docs("androidx.lifecycle:lifecycle-common-java8:2.6.0-alpha05")
     docs("androidx.lifecycle:lifecycle-extensions:2.2.0")
-    docs("androidx.lifecycle:lifecycle-livedata:2.6.0-alpha04")
-    docs("androidx.lifecycle:lifecycle-livedata-core:2.6.0-alpha04")
-    docs("androidx.lifecycle:lifecycle-livedata-core-ktx:2.6.0-alpha04")
-    docs("androidx.lifecycle:lifecycle-livedata-ktx:2.6.0-alpha04")
-    docs("androidx.lifecycle:lifecycle-process:2.6.0-alpha04")
-    docs("androidx.lifecycle:lifecycle-reactivestreams:2.6.0-alpha04")
-    docs("androidx.lifecycle:lifecycle-reactivestreams-ktx:2.6.0-alpha04")
-    docs("androidx.lifecycle:lifecycle-runtime:2.6.0-alpha04")
-    docs("androidx.lifecycle:lifecycle-runtime-compose:2.6.0-alpha04")
-    samples("androidx.lifecycle:lifecycle-runtime-compose-samples:2.6.0-alpha04")
-    docs("androidx.lifecycle:lifecycle-runtime-ktx:2.6.0-alpha04")
-    docs("androidx.lifecycle:lifecycle-runtime-testing:2.6.0-alpha04")
-    docs("androidx.lifecycle:lifecycle-service:2.6.0-alpha04")
-    docs("androidx.lifecycle:lifecycle-viewmodel:2.6.0-alpha04")
-    docs("androidx.lifecycle:lifecycle-viewmodel-compose:2.6.0-alpha04")
-    samples("androidx.lifecycle:lifecycle-viewmodel-compose-samples:2.6.0-alpha04")
-    docs("androidx.lifecycle:lifecycle-viewmodel-ktx:2.6.0-alpha04")
-    docs("androidx.lifecycle:lifecycle-viewmodel-savedstate:2.6.0-alpha04")
+    docs("androidx.lifecycle:lifecycle-livedata:2.6.0-alpha05")
+    docs("androidx.lifecycle:lifecycle-livedata-core:2.6.0-alpha05")
+    docs("androidx.lifecycle:lifecycle-livedata-core-ktx:2.6.0-alpha05")
+    docs("androidx.lifecycle:lifecycle-livedata-ktx:2.6.0-alpha05")
+    docs("androidx.lifecycle:lifecycle-process:2.6.0-alpha05")
+    docs("androidx.lifecycle:lifecycle-reactivestreams:2.6.0-alpha05")
+    docs("androidx.lifecycle:lifecycle-reactivestreams-ktx:2.6.0-alpha05")
+    docs("androidx.lifecycle:lifecycle-runtime:2.6.0-alpha05")
+    docs("androidx.lifecycle:lifecycle-runtime-compose:2.6.0-alpha05")
+    samples("androidx.lifecycle:lifecycle-runtime-compose-samples:2.6.0-alpha05")
+    docs("androidx.lifecycle:lifecycle-runtime-ktx:2.6.0-alpha05")
+    docs("androidx.lifecycle:lifecycle-runtime-testing:2.6.0-alpha05")
+    docs("androidx.lifecycle:lifecycle-service:2.6.0-alpha05")
+    docs("androidx.lifecycle:lifecycle-viewmodel:2.6.0-alpha05")
+    docs("androidx.lifecycle:lifecycle-viewmodel-compose:2.6.0-alpha05")
+    samples("androidx.lifecycle:lifecycle-viewmodel-compose-samples:2.6.0-alpha05")
+    docs("androidx.lifecycle:lifecycle-viewmodel-ktx:2.6.0-alpha05")
+    docs("androidx.lifecycle:lifecycle-viewmodel-savedstate:2.6.0-alpha05")
     docs("androidx.loader:loader:1.1.0")
     docs("androidx.localbroadcastmanager:localbroadcastmanager:1.1.0")
     docs("androidx.media2:media2-common:1.2.1")
@@ -209,22 +210,22 @@
     docs("androidx.media2:media2-session:1.2.1")
     docs("androidx.media2:media2-widget:1.2.1")
     docs("androidx.media:media:1.6.0")
-    docs("androidx.mediarouter:mediarouter:1.4.0-alpha01")
-    docs("androidx.mediarouter:mediarouter-testing:1.4.0-alpha01")
+    docs("androidx.mediarouter:mediarouter:1.4.0-beta01")
+    docs("androidx.mediarouter:mediarouter-testing:1.4.0-beta01")
     docs("androidx.metrics:metrics-performance:1.0.0-alpha03")
-    docs("androidx.navigation:navigation-common:2.6.0-alpha04")
-    docs("androidx.navigation:navigation-common-ktx:2.6.0-alpha04")
-    docs("androidx.navigation:navigation-compose:2.6.0-alpha04")
-    samples("androidx.navigation:navigation-compose-samples:2.6.0-alpha04")
-    docs("androidx.navigation:navigation-dynamic-features-fragment:2.6.0-alpha04")
-    docs("androidx.navigation:navigation-dynamic-features-runtime:2.6.0-alpha04")
-    docs("androidx.navigation:navigation-fragment:2.6.0-alpha04")
-    docs("androidx.navigation:navigation-fragment-ktx:2.6.0-alpha04")
-    docs("androidx.navigation:navigation-runtime:2.6.0-alpha04")
-    docs("androidx.navigation:navigation-runtime-ktx:2.6.0-alpha04")
-    docs("androidx.navigation:navigation-testing:2.6.0-alpha04")
-    docs("androidx.navigation:navigation-ui:2.6.0-alpha04")
-    docs("androidx.navigation:navigation-ui-ktx:2.6.0-alpha04")
+    docs("androidx.navigation:navigation-common:2.6.0-alpha05")
+    docs("androidx.navigation:navigation-common-ktx:2.6.0-alpha05")
+    docs("androidx.navigation:navigation-compose:2.6.0-alpha05")
+    samples("androidx.navigation:navigation-compose-samples:2.6.0-alpha05")
+    docs("androidx.navigation:navigation-dynamic-features-fragment:2.6.0-alpha05")
+    docs("androidx.navigation:navigation-dynamic-features-runtime:2.6.0-alpha05")
+    docs("androidx.navigation:navigation-fragment:2.6.0-alpha05")
+    docs("androidx.navigation:navigation-fragment-ktx:2.6.0-alpha05")
+    docs("androidx.navigation:navigation-runtime:2.6.0-alpha05")
+    docs("androidx.navigation:navigation-runtime-ktx:2.6.0-alpha05")
+    docs("androidx.navigation:navigation-testing:2.6.0-alpha05")
+    docs("androidx.navigation:navigation-ui:2.6.0-alpha05")
+    docs("androidx.navigation:navigation-ui-ktx:2.6.0-alpha05")
     docs("androidx.paging:paging-common:3.2.0-alpha03")
     docs("androidx.paging:paging-common-ktx:3.2.0-alpha03")
     docs("androidx.paging:paging-compose:1.0.0-alpha17")
@@ -320,38 +321,41 @@
     docs("androidx.versionedparcelable:versionedparcelable:1.1.1")
     docs("androidx.viewpager2:viewpager2:1.1.0-beta01")
     docs("androidx.viewpager:viewpager:1.1.0-alpha01")
-    docs("androidx.wear.compose:compose-foundation:1.2.0-alpha02")
-    samples("androidx.wear.compose:compose-foundation-samples:1.2.0-alpha02")
-    docs("androidx.wear.compose:compose-material:1.2.0-alpha02")
-    docs("androidx.wear.compose:compose-material-core:1.2.0-alpha02")
-    samples("androidx.wear.compose:compose-material-samples:1.2.0-alpha02")
-    docs("androidx.wear.compose:compose-navigation:1.2.0-alpha02")
-    samples("androidx.wear.compose:compose-navigation-samples:1.2.0-alpha02")
-    docs("androidx.wear.protolayout:protolayout:1.0.0-alpha01")
-    docs("androidx.wear.protolayout:protolayout-expression:1.0.0-alpha01")
-    docs("androidx.wear.protolayout:protolayout-proto:1.0.0-alpha01")
+    docs("androidx.wear.compose:compose-foundation:1.2.0-alpha03")
+    samples("androidx.wear.compose:compose-foundation-samples:1.2.0-alpha03")
+    docs("androidx.wear.compose:compose-material:1.2.0-alpha03")
+    docs("androidx.wear.compose:compose-material-core:1.2.0-alpha03")
+    samples("androidx.wear.compose:compose-material-samples:1.2.0-alpha03")
+    docs("androidx.wear.compose:compose-material3:1.0.0-alpha01")
+    samples("androidx.wear.compose:compose-material3-samples:1.2.0-alpha03")
+    docs("androidx.wear.compose:compose-navigation:1.2.0-alpha03")
+    samples("androidx.wear.compose:compose-navigation-samples:1.2.0-alpha03")
+    docs("androidx.wear.protolayout:protolayout:1.0.0-alpha02")
+    docs("androidx.wear.protolayout:protolayout-expression:1.0.0-alpha02")
+    docs("androidx.wear.protolayout:protolayout-proto:1.0.0-alpha02")
+    docs("androidx.wear.protolayout:protolayout-renderer:1.0.0-alpha02")
     docs("androidx.wear.tiles:tiles:1.1.0")
     docs("androidx.wear.tiles:tiles-material:1.1.0")
     docs("androidx.wear.tiles:tiles-proto:1.1.0")
     docs("androidx.wear.tiles:tiles-renderer:1.1.0")
     docs("androidx.wear.tiles:tiles-testing:1.1.0")
-    docs("androidx.wear.watchface:watchface:1.2.0-alpha05")
-    docs("androidx.wear.watchface:watchface-client:1.2.0-alpha05")
-    docs("androidx.wear.watchface:watchface-client-guava:1.2.0-alpha05")
-    docs("androidx.wear.watchface:watchface-complications:1.2.0-alpha05")
-    docs("androidx.wear.watchface:watchface-complications-data:1.2.0-alpha05")
-    docs("androidx.wear.watchface:watchface-complications-data-source:1.2.0-alpha05")
-    docs("androidx.wear.watchface:watchface-complications-data-source-ktx:1.2.0-alpha05")
-    samples("androidx.wear.watchface:watchface-complications-permission-dialogs-sample:1.2.0-alpha05")
-    docs("androidx.wear.watchface:watchface-complications-rendering:1.2.0-alpha05")
-    docs("androidx.wear.watchface:watchface-data:1.2.0-alpha05")
-    docs("androidx.wear.watchface:watchface-editor:1.2.0-alpha05")
-    docs("androidx.wear.watchface:watchface-editor-guava:1.2.0-alpha05")
-    samples("androidx.wear.watchface:watchface-editor-samples:1.2.0-alpha05")
-    docs("androidx.wear.watchface:watchface-guava:1.2.0-alpha05")
-    samples("androidx.wear.watchface:watchface-samples:1.2.0-alpha05")
-    docs("androidx.wear.watchface:watchface-style:1.2.0-alpha05")
-    docs("androidx.wear:wear:1.3.0-alpha03")
+    docs("androidx.wear.watchface:watchface:1.2.0-alpha06")
+    docs("androidx.wear.watchface:watchface-client:1.2.0-alpha06")
+    docs("androidx.wear.watchface:watchface-client-guava:1.2.0-alpha06")
+    docs("androidx.wear.watchface:watchface-complications:1.2.0-alpha06")
+    docs("androidx.wear.watchface:watchface-complications-data:1.2.0-alpha06")
+    docs("androidx.wear.watchface:watchface-complications-data-source:1.2.0-alpha06")
+    docs("androidx.wear.watchface:watchface-complications-data-source-ktx:1.2.0-alpha06")
+    samples("androidx.wear.watchface:watchface-complications-permission-dialogs-sample:1.2.0-alpha06")
+    docs("androidx.wear.watchface:watchface-complications-rendering:1.2.0-alpha06")
+    docs("androidx.wear.watchface:watchface-data:1.2.0-alpha06")
+    docs("androidx.wear.watchface:watchface-editor:1.2.0-alpha06")
+    docs("androidx.wear.watchface:watchface-editor-guava:1.2.0-alpha06")
+    samples("androidx.wear.watchface:watchface-editor-samples:1.2.0-alpha06")
+    docs("androidx.wear.watchface:watchface-guava:1.2.0-alpha06")
+    samples("androidx.wear.watchface:watchface-samples:1.2.0-alpha06")
+    docs("androidx.wear.watchface:watchface-style:1.2.0-alpha06")
+    docs("androidx.wear:wear:1.3.0-alpha04")
     stubs(fileTree(dir: "../wear/wear_stubs/", include: ["com.google.android.wearable-stubs.jar"]))
     docs("androidx.wear:wear-ongoing:1.1.0-alpha01")
     docs("androidx.wear:wear-phone-interactions:1.1.0-alpha03")
@@ -359,7 +363,7 @@
     docs("androidx.wear:wear-input:1.2.0-alpha02")
     samples("androidx.wear:wear-input-samples:1.2.0-alpha01")
     docs("androidx.wear:wear-input-testing:1.2.0-alpha02")
-    docs("androidx.webkit:webkit:1.6.0-rc01")
+    docs("androidx.webkit:webkit:1.7.0-alpha01")
     docs("androidx.window:window:1.1.0-alpha04")
     stubs(fileTree(dir: "../window/stubs/", include: ["window-sidecar-release-0.1.0-alpha01.aar"]))
     docs("androidx.window:window-core:1.1.0-alpha04")
diff --git a/docs-tip-of-tree/build.gradle b/docs-tip-of-tree/build.gradle
index f8d7b91..d61d7f9 100644
--- a/docs-tip-of-tree/build.gradle
+++ b/docs-tip-of-tree/build.gradle
@@ -19,6 +19,7 @@
     kmpDocs(project(":annotation:annotation"))
     docs(project(":annotation:annotation-experimental"))
     docs(project(":appactions:interaction:interaction-proto"))
+    docs(project(":appactions:interaction:interaction-capabilities-core"))
     docs(project(":appcompat:appcompat"))
     docs(project(":appcompat:appcompat-resources"))
     docs(project(":appsearch:appsearch"))
@@ -249,6 +250,8 @@
     docs(project(":preference:preference"))
     docs(project(":preference:preference-ktx"))
     docs(project(":print:print"))
+    docs(project(":privacysandbox:ads:ads-adservices"))
+    docs(project(":privacysandbox:ads:ads-adservices-java"))
     docs(project(":privacysandbox:sdkruntime:sdkruntime-client"))
     docs(project(":privacysandbox:sdkruntime:sdkruntime-core"))
     docs(project(":privacysandbox:tools:tools"))
diff --git a/drawerlayout/drawerlayout/api/1.2.0-beta01.txt b/drawerlayout/drawerlayout/api/1.2.0-beta01.txt
new file mode 100644
index 0000000..d638832
--- /dev/null
+++ b/drawerlayout/drawerlayout/api/1.2.0-beta01.txt
@@ -0,0 +1,85 @@
+// Signature format: 4.0
+package androidx.drawerlayout.widget {
+
+  public class DrawerLayout extends android.view.ViewGroup implements androidx.customview.widget.Openable {
+    ctor public DrawerLayout(android.content.Context);
+    ctor public DrawerLayout(android.content.Context, android.util.AttributeSet?);
+    ctor public DrawerLayout(android.content.Context, android.util.AttributeSet?, int);
+    method public void addDrawerListener(androidx.drawerlayout.widget.DrawerLayout.DrawerListener);
+    method public void close();
+    method public void closeDrawer(android.view.View);
+    method public void closeDrawer(android.view.View, boolean);
+    method public void closeDrawer(int);
+    method public void closeDrawer(int, boolean);
+    method public void closeDrawers();
+    method public float getDrawerElevation();
+    method public int getDrawerLockMode(int);
+    method public int getDrawerLockMode(android.view.View);
+    method public CharSequence? getDrawerTitle(int);
+    method public android.graphics.drawable.Drawable? getStatusBarBackgroundDrawable();
+    method public boolean isDrawerOpen(android.view.View);
+    method public boolean isDrawerOpen(int);
+    method public boolean isDrawerVisible(android.view.View);
+    method public boolean isDrawerVisible(int);
+    method public boolean isOpen();
+    method public void onDraw(android.graphics.Canvas);
+    method public void open();
+    method public void openDrawer(android.view.View);
+    method public void openDrawer(android.view.View, boolean);
+    method public void openDrawer(int);
+    method public void openDrawer(int, boolean);
+    method public void removeDrawerListener(androidx.drawerlayout.widget.DrawerLayout.DrawerListener);
+    method public void setDrawerElevation(float);
+    method @Deprecated public void setDrawerListener(androidx.drawerlayout.widget.DrawerLayout.DrawerListener!);
+    method public void setDrawerLockMode(int);
+    method public void setDrawerLockMode(int, int);
+    method public void setDrawerLockMode(int, android.view.View);
+    method public void setDrawerShadow(android.graphics.drawable.Drawable?, int);
+    method public void setDrawerShadow(@DrawableRes int, int);
+    method public void setDrawerTitle(int, CharSequence?);
+    method public void setScrimColor(@ColorInt int);
+    method public void setStatusBarBackground(android.graphics.drawable.Drawable?);
+    method public void setStatusBarBackground(int);
+    method public void setStatusBarBackgroundColor(@ColorInt int);
+    field public static final int LOCK_MODE_LOCKED_CLOSED = 1; // 0x1
+    field public static final int LOCK_MODE_LOCKED_OPEN = 2; // 0x2
+    field public static final int LOCK_MODE_UNDEFINED = 3; // 0x3
+    field public static final int LOCK_MODE_UNLOCKED = 0; // 0x0
+    field public static final int STATE_DRAGGING = 1; // 0x1
+    field public static final int STATE_IDLE = 0; // 0x0
+    field public static final int STATE_SETTLING = 2; // 0x2
+  }
+
+  public static interface DrawerLayout.DrawerListener {
+    method public void onDrawerClosed(android.view.View);
+    method public void onDrawerOpened(android.view.View);
+    method public void onDrawerSlide(android.view.View, float);
+    method public void onDrawerStateChanged(int);
+  }
+
+  public static class DrawerLayout.LayoutParams extends android.view.ViewGroup.MarginLayoutParams {
+    ctor public DrawerLayout.LayoutParams(android.content.Context, android.util.AttributeSet?);
+    ctor public DrawerLayout.LayoutParams(int, int);
+    ctor public DrawerLayout.LayoutParams(int, int, int);
+    ctor public DrawerLayout.LayoutParams(androidx.drawerlayout.widget.DrawerLayout.LayoutParams);
+    ctor public DrawerLayout.LayoutParams(android.view.ViewGroup.LayoutParams);
+    ctor public DrawerLayout.LayoutParams(android.view.ViewGroup.MarginLayoutParams);
+    field public int gravity;
+  }
+
+  protected static class DrawerLayout.SavedState extends androidx.customview.view.AbsSavedState {
+    ctor public DrawerLayout.SavedState(android.os.Parcel, ClassLoader?);
+    ctor public DrawerLayout.SavedState(android.os.Parcelable);
+    field public static final android.os.Parcelable.Creator<androidx.drawerlayout.widget.DrawerLayout.SavedState!>! CREATOR;
+  }
+
+  public abstract static class DrawerLayout.SimpleDrawerListener implements androidx.drawerlayout.widget.DrawerLayout.DrawerListener {
+    ctor public DrawerLayout.SimpleDrawerListener();
+    method public void onDrawerClosed(android.view.View);
+    method public void onDrawerOpened(android.view.View);
+    method public void onDrawerSlide(android.view.View, float);
+    method public void onDrawerStateChanged(int);
+  }
+
+}
+
diff --git a/drawerlayout/drawerlayout/api/public_plus_experimental_1.2.0-beta01.txt b/drawerlayout/drawerlayout/api/public_plus_experimental_1.2.0-beta01.txt
new file mode 100644
index 0000000..d638832
--- /dev/null
+++ b/drawerlayout/drawerlayout/api/public_plus_experimental_1.2.0-beta01.txt
@@ -0,0 +1,85 @@
+// Signature format: 4.0
+package androidx.drawerlayout.widget {
+
+  public class DrawerLayout extends android.view.ViewGroup implements androidx.customview.widget.Openable {
+    ctor public DrawerLayout(android.content.Context);
+    ctor public DrawerLayout(android.content.Context, android.util.AttributeSet?);
+    ctor public DrawerLayout(android.content.Context, android.util.AttributeSet?, int);
+    method public void addDrawerListener(androidx.drawerlayout.widget.DrawerLayout.DrawerListener);
+    method public void close();
+    method public void closeDrawer(android.view.View);
+    method public void closeDrawer(android.view.View, boolean);
+    method public void closeDrawer(int);
+    method public void closeDrawer(int, boolean);
+    method public void closeDrawers();
+    method public float getDrawerElevation();
+    method public int getDrawerLockMode(int);
+    method public int getDrawerLockMode(android.view.View);
+    method public CharSequence? getDrawerTitle(int);
+    method public android.graphics.drawable.Drawable? getStatusBarBackgroundDrawable();
+    method public boolean isDrawerOpen(android.view.View);
+    method public boolean isDrawerOpen(int);
+    method public boolean isDrawerVisible(android.view.View);
+    method public boolean isDrawerVisible(int);
+    method public boolean isOpen();
+    method public void onDraw(android.graphics.Canvas);
+    method public void open();
+    method public void openDrawer(android.view.View);
+    method public void openDrawer(android.view.View, boolean);
+    method public void openDrawer(int);
+    method public void openDrawer(int, boolean);
+    method public void removeDrawerListener(androidx.drawerlayout.widget.DrawerLayout.DrawerListener);
+    method public void setDrawerElevation(float);
+    method @Deprecated public void setDrawerListener(androidx.drawerlayout.widget.DrawerLayout.DrawerListener!);
+    method public void setDrawerLockMode(int);
+    method public void setDrawerLockMode(int, int);
+    method public void setDrawerLockMode(int, android.view.View);
+    method public void setDrawerShadow(android.graphics.drawable.Drawable?, int);
+    method public void setDrawerShadow(@DrawableRes int, int);
+    method public void setDrawerTitle(int, CharSequence?);
+    method public void setScrimColor(@ColorInt int);
+    method public void setStatusBarBackground(android.graphics.drawable.Drawable?);
+    method public void setStatusBarBackground(int);
+    method public void setStatusBarBackgroundColor(@ColorInt int);
+    field public static final int LOCK_MODE_LOCKED_CLOSED = 1; // 0x1
+    field public static final int LOCK_MODE_LOCKED_OPEN = 2; // 0x2
+    field public static final int LOCK_MODE_UNDEFINED = 3; // 0x3
+    field public static final int LOCK_MODE_UNLOCKED = 0; // 0x0
+    field public static final int STATE_DRAGGING = 1; // 0x1
+    field public static final int STATE_IDLE = 0; // 0x0
+    field public static final int STATE_SETTLING = 2; // 0x2
+  }
+
+  public static interface DrawerLayout.DrawerListener {
+    method public void onDrawerClosed(android.view.View);
+    method public void onDrawerOpened(android.view.View);
+    method public void onDrawerSlide(android.view.View, float);
+    method public void onDrawerStateChanged(int);
+  }
+
+  public static class DrawerLayout.LayoutParams extends android.view.ViewGroup.MarginLayoutParams {
+    ctor public DrawerLayout.LayoutParams(android.content.Context, android.util.AttributeSet?);
+    ctor public DrawerLayout.LayoutParams(int, int);
+    ctor public DrawerLayout.LayoutParams(int, int, int);
+    ctor public DrawerLayout.LayoutParams(androidx.drawerlayout.widget.DrawerLayout.LayoutParams);
+    ctor public DrawerLayout.LayoutParams(android.view.ViewGroup.LayoutParams);
+    ctor public DrawerLayout.LayoutParams(android.view.ViewGroup.MarginLayoutParams);
+    field public int gravity;
+  }
+
+  protected static class DrawerLayout.SavedState extends androidx.customview.view.AbsSavedState {
+    ctor public DrawerLayout.SavedState(android.os.Parcel, ClassLoader?);
+    ctor public DrawerLayout.SavedState(android.os.Parcelable);
+    field public static final android.os.Parcelable.Creator<androidx.drawerlayout.widget.DrawerLayout.SavedState!>! CREATOR;
+  }
+
+  public abstract static class DrawerLayout.SimpleDrawerListener implements androidx.drawerlayout.widget.DrawerLayout.DrawerListener {
+    ctor public DrawerLayout.SimpleDrawerListener();
+    method public void onDrawerClosed(android.view.View);
+    method public void onDrawerOpened(android.view.View);
+    method public void onDrawerSlide(android.view.View, float);
+    method public void onDrawerStateChanged(int);
+  }
+
+}
+
diff --git a/drawerlayout/drawerlayout/api/res-1.2.0-beta01.txt b/drawerlayout/drawerlayout/api/res-1.2.0-beta01.txt
new file mode 100644
index 0000000..3756729
--- /dev/null
+++ b/drawerlayout/drawerlayout/api/res-1.2.0-beta01.txt
@@ -0,0 +1,2 @@
+attr drawerLayoutStyle
+attr elevation
diff --git a/drawerlayout/drawerlayout/api/restricted_1.2.0-beta01.txt b/drawerlayout/drawerlayout/api/restricted_1.2.0-beta01.txt
new file mode 100644
index 0000000..2e81e63
--- /dev/null
+++ b/drawerlayout/drawerlayout/api/restricted_1.2.0-beta01.txt
@@ -0,0 +1,86 @@
+// Signature format: 4.0
+package androidx.drawerlayout.widget {
+
+  public class DrawerLayout extends android.view.ViewGroup implements androidx.customview.widget.Openable {
+    ctor public DrawerLayout(android.content.Context);
+    ctor public DrawerLayout(android.content.Context, android.util.AttributeSet?);
+    ctor public DrawerLayout(android.content.Context, android.util.AttributeSet?, int);
+    method public void addDrawerListener(androidx.drawerlayout.widget.DrawerLayout.DrawerListener);
+    method public void close();
+    method public void closeDrawer(android.view.View);
+    method public void closeDrawer(android.view.View, boolean);
+    method public void closeDrawer(int);
+    method public void closeDrawer(int, boolean);
+    method public void closeDrawers();
+    method public float getDrawerElevation();
+    method public int getDrawerLockMode(int);
+    method public int getDrawerLockMode(android.view.View);
+    method public CharSequence? getDrawerTitle(int);
+    method public android.graphics.drawable.Drawable? getStatusBarBackgroundDrawable();
+    method public boolean isDrawerOpen(android.view.View);
+    method public boolean isDrawerOpen(int);
+    method public boolean isDrawerVisible(android.view.View);
+    method public boolean isDrawerVisible(int);
+    method public boolean isOpen();
+    method public void onDraw(android.graphics.Canvas);
+    method public void open();
+    method public void openDrawer(android.view.View);
+    method public void openDrawer(android.view.View, boolean);
+    method public void openDrawer(int);
+    method public void openDrawer(int, boolean);
+    method public void removeDrawerListener(androidx.drawerlayout.widget.DrawerLayout.DrawerListener);
+    method @RestrictTo(androidx.annotation.RestrictTo.Scope.LIBRARY_GROUP_PREFIX) public void setChildInsets(androidx.core.view.WindowInsetsCompat?, boolean);
+    method public void setDrawerElevation(float);
+    method @Deprecated public void setDrawerListener(androidx.drawerlayout.widget.DrawerLayout.DrawerListener!);
+    method public void setDrawerLockMode(int);
+    method public void setDrawerLockMode(int, int);
+    method public void setDrawerLockMode(int, android.view.View);
+    method public void setDrawerShadow(android.graphics.drawable.Drawable?, int);
+    method public void setDrawerShadow(@DrawableRes int, int);
+    method public void setDrawerTitle(int, CharSequence?);
+    method public void setScrimColor(@ColorInt int);
+    method public void setStatusBarBackground(android.graphics.drawable.Drawable?);
+    method public void setStatusBarBackground(int);
+    method public void setStatusBarBackgroundColor(@ColorInt int);
+    field public static final int LOCK_MODE_LOCKED_CLOSED = 1; // 0x1
+    field public static final int LOCK_MODE_LOCKED_OPEN = 2; // 0x2
+    field public static final int LOCK_MODE_UNDEFINED = 3; // 0x3
+    field public static final int LOCK_MODE_UNLOCKED = 0; // 0x0
+    field public static final int STATE_DRAGGING = 1; // 0x1
+    field public static final int STATE_IDLE = 0; // 0x0
+    field public static final int STATE_SETTLING = 2; // 0x2
+  }
+
+  public static interface DrawerLayout.DrawerListener {
+    method public void onDrawerClosed(android.view.View);
+    method public void onDrawerOpened(android.view.View);
+    method public void onDrawerSlide(android.view.View, float);
+    method public void onDrawerStateChanged(int);
+  }
+
+  public static class DrawerLayout.LayoutParams extends android.view.ViewGroup.MarginLayoutParams {
+    ctor public DrawerLayout.LayoutParams(android.content.Context, android.util.AttributeSet?);
+    ctor public DrawerLayout.LayoutParams(int, int);
+    ctor public DrawerLayout.LayoutParams(int, int, int);
+    ctor public DrawerLayout.LayoutParams(androidx.drawerlayout.widget.DrawerLayout.LayoutParams);
+    ctor public DrawerLayout.LayoutParams(android.view.ViewGroup.LayoutParams);
+    ctor public DrawerLayout.LayoutParams(android.view.ViewGroup.MarginLayoutParams);
+    field public int gravity;
+  }
+
+  protected static class DrawerLayout.SavedState extends androidx.customview.view.AbsSavedState {
+    ctor public DrawerLayout.SavedState(android.os.Parcel, ClassLoader?);
+    ctor public DrawerLayout.SavedState(android.os.Parcelable);
+    field public static final android.os.Parcelable.Creator<androidx.drawerlayout.widget.DrawerLayout.SavedState!>! CREATOR;
+  }
+
+  public abstract static class DrawerLayout.SimpleDrawerListener implements androidx.drawerlayout.widget.DrawerLayout.DrawerListener {
+    ctor public DrawerLayout.SimpleDrawerListener();
+    method public void onDrawerClosed(android.view.View);
+    method public void onDrawerOpened(android.view.View);
+    method public void onDrawerSlide(android.view.View, float);
+    method public void onDrawerStateChanged(int);
+  }
+
+}
+
diff --git a/drawerlayout/drawerlayout/build.gradle b/drawerlayout/drawerlayout/build.gradle
index 338299b..6405b31 100644
--- a/drawerlayout/drawerlayout/build.gradle
+++ b/drawerlayout/drawerlayout/build.gradle
@@ -9,7 +9,7 @@
 dependencies {
     api("androidx.annotation:annotation:1.2.0")
     api("androidx.core:core:1.2.0")
-    api(project(":customview:customview"))
+    api("androidx.customview:customview:1.1.0")
 
     androidTestImplementation(libs.kotlinStdlib)
     androidTestImplementation(libs.testExtJunit)
diff --git a/emoji2/emoji2-emojipicker/src/main/java/androidx/emoji2/emojipicker/EmojiPickerConstants.kt b/emoji2/emoji2-emojipicker/src/main/java/androidx/emoji2/emojipicker/EmojiPickerConstants.kt
index 989a8e3..479e1ff 100644
--- a/emoji2/emoji2-emojipicker/src/main/java/androidx/emoji2/emojipicker/EmojiPickerConstants.kt
+++ b/emoji2/emoji2-emojipicker/src/main/java/androidx/emoji2/emojipicker/EmojiPickerConstants.kt
@@ -30,4 +30,9 @@
 
     // The max pool size of the Emoji ItemType in RecyclerViewPool.
     const val EMOJI_VIEW_POOL_SIZE = 100
+
+    const val ADD_VIEW_EXCEPTION_MESSAGE = "Adding views to the EmojiPickerView is unsupported"
+
+    const val REMOVE_VIEW_EXCEPTION_MESSAGE =
+        "Removing views from the EmojiPickerView is unsupported"
 }
\ No newline at end of file
diff --git a/emoji2/emoji2-emojipicker/src/main/java/androidx/emoji2/emojipicker/EmojiPickerView.kt b/emoji2/emoji2-emojipicker/src/main/java/androidx/emoji2/emojipicker/EmojiPickerView.kt
index ef30c3b..79ce7be 100644
--- a/emoji2/emoji2-emojipicker/src/main/java/androidx/emoji2/emojipicker/EmojiPickerView.kt
+++ b/emoji2/emoji2-emojipicker/src/main/java/androidx/emoji2/emojipicker/EmojiPickerView.kt
@@ -20,6 +20,7 @@
 import android.content.res.TypedArray
 import android.util.AttributeSet
 import android.view.View
+import android.view.ViewGroup
 import android.widget.FrameLayout
 import androidx.core.util.Consumer
 import androidx.core.view.ViewCompat
@@ -46,9 +47,10 @@
     /**
      * The number of rows of the emoji picker.
      *
-     * Default value will be used if emojiGridRows is set to non-positive value. Float value
-     * indicates that we will display partial of the last row and have content down, so the users
-     * get the idea that they can scroll down for more contents.
+     * Default value([EmojiPickerConstants.DEFAULT_BODY_ROWS]: 7.5) will be used if emojiGridRows
+     * is set to non-positive value. Float value indicates that we will display partial of the last
+     * row and have content down, so the users get the idea that they can scroll down for more
+     * contents.
      * @attr ref androidx.emoji2.emojipicker.R.styleable.EmojiPickerView_emojiGridRows
      */
     var emojiGridRows: Float = EmojiPickerConstants.DEFAULT_BODY_ROWS
@@ -63,7 +65,8 @@
     /**
      * The number of columns of the emoji picker.
      *
-     * Default value will be used if emojiGridColumns is set to non-positive value.
+     * Default value([EmojiPickerConstants.DEFAULT_BODY_COLUMNS]: 9) will be used if
+     * emojiGridColumns is set to non-positive value.
      * @attr ref androidx.emoji2.emojipicker.R.styleable.EmojiPickerView_emojiGridColumns
      */
     var emojiGridColumns: Int = EmojiPickerConstants.DEFAULT_BODY_COLUMNS
@@ -180,7 +183,7 @@
             })
 
         // clear view's children in case of resetting layout
-        removeAllViews()
+        super.removeAllViews()
         with(inflate(context, R.layout.emoji_picker, this)) {
             // set headerView
             ViewCompat.requireViewById<RecyclerView>(this, R.id.emoji_picker_header).apply {
@@ -259,14 +262,110 @@
     }
 
     /**
-     * Disallow clients to add view to the EmojiPickerView
+     * The following functions disallow clients to add view to the EmojiPickerView
      *
      * @param child the child view to be added
      * @throws UnsupportedOperationException
      */
     override fun addView(child: View?) {
-        throw UnsupportedOperationException(
-            "Adding views to the EmojiPickerView is unsupported"
-        )
+        if (childCount > 0)
+            throw UnsupportedOperationException(EmojiPickerConstants.ADD_VIEW_EXCEPTION_MESSAGE)
+        else super.addView(child)
+    }
+
+    /**
+     * @param child
+     * @param params
+     * @throws UnsupportedOperationException
+     */
+    override fun addView(child: View?, params: ViewGroup.LayoutParams?) {
+        if (childCount > 0)
+            throw UnsupportedOperationException(EmojiPickerConstants.ADD_VIEW_EXCEPTION_MESSAGE)
+        else super.addView(child, params)
+    }
+
+    /**
+     * @param child
+     * @param index
+     * @throws UnsupportedOperationException
+     */
+    override fun addView(child: View?, index: Int) {
+        if (childCount > 0)
+            throw UnsupportedOperationException(EmojiPickerConstants.ADD_VIEW_EXCEPTION_MESSAGE)
+        else super.addView(child, index)
+    }
+
+    /**
+     * @param child
+     * @param index
+     * @param params
+     * @throws UnsupportedOperationException
+     */
+    override fun addView(child: View?, index: Int, params: ViewGroup.LayoutParams?) {
+        if (childCount > 0)
+            throw UnsupportedOperationException(EmojiPickerConstants.ADD_VIEW_EXCEPTION_MESSAGE)
+        else super.addView(child, index, params)
+    }
+
+    /**
+     * @param child
+     * @param width
+     * @param height
+     * @throws UnsupportedOperationException
+     */
+    override fun addView(child: View?, width: Int, height: Int) {
+        if (childCount > 0)
+            throw UnsupportedOperationException(EmojiPickerConstants.ADD_VIEW_EXCEPTION_MESSAGE)
+        else super.addView(child, width, height)
+    }
+
+    /**
+     * The following functions disallow clients to remove view from the EmojiPickerView
+     * @throws UnsupportedOperationException
+     */
+    override fun removeAllViews() {
+        throw UnsupportedOperationException(EmojiPickerConstants.REMOVE_VIEW_EXCEPTION_MESSAGE)
+    }
+
+    /**
+     * @param child
+     * @throws UnsupportedOperationException
+     */
+    override fun removeView(child: View?) {
+        throw UnsupportedOperationException(EmojiPickerConstants.REMOVE_VIEW_EXCEPTION_MESSAGE)
+    }
+
+    /**
+     * @param index
+     * @throws UnsupportedOperationException
+     */
+    override fun removeViewAt(index: Int) {
+        throw UnsupportedOperationException(EmojiPickerConstants.REMOVE_VIEW_EXCEPTION_MESSAGE)
+    }
+
+    /**
+     * @param child
+     * @throws UnsupportedOperationException
+     */
+    override fun removeViewInLayout(child: View?) {
+        throw UnsupportedOperationException(EmojiPickerConstants.REMOVE_VIEW_EXCEPTION_MESSAGE)
+    }
+
+    /**
+     * @param start
+     * @param count
+     * @throws UnsupportedOperationException
+     */
+    override fun removeViews(start: Int, count: Int) {
+        throw UnsupportedOperationException(EmojiPickerConstants.REMOVE_VIEW_EXCEPTION_MESSAGE)
+    }
+
+    /**
+     * @param start
+     * @param count
+     * @throws UnsupportedOperationException
+     */
+    override fun removeViewsInLayout(start: Int, count: Int) {
+        throw UnsupportedOperationException(EmojiPickerConstants.REMOVE_VIEW_EXCEPTION_MESSAGE)
     }
 }
diff --git a/emoji2/emoji2/src/main/java/androidx/emoji2/text/EmojiCompat.java b/emoji2/emoji2/src/main/java/androidx/emoji2/text/EmojiCompat.java
index 6426ad5..9604796 100644
--- a/emoji2/emoji2/src/main/java/androidx/emoji2/text/EmojiCompat.java
+++ b/emoji2/emoji2/src/main/java/androidx/emoji2/text/EmojiCompat.java
@@ -1189,13 +1189,14 @@
         /**
          * Create EmojiSpan instance.
          *
-         * @param metadata EmojiMetadata instance, which can draw the emoji onto a Canvas.
+         * @param rasterizer TypefaceEmojiRasterizer instance, which can draw the emoji onto a
+         *                   Canvas.
          *
-         * @return EmojiSpan instance that can use EmojiMetadata to draw emoji.
+         * @return EmojiSpan instance that can use TypefaceEmojiRasterizer to draw emoji.
          */
         @RequiresApi(19)
         @NonNull
-        EmojiSpan createSpan(@NonNull TypefaceEmojiRasterizer metadata);
+        EmojiSpan createSpan(@NonNull TypefaceEmojiRasterizer rasterizer);
     }
 
 
@@ -1208,15 +1209,16 @@
         /**
          * Returns a TypefaceEmojiSpan.
          *
-         * @param metadata EmojiMetadata instance, which can draw the emoji onto a Canvas.
+         * @param rasterizer TypefaceEmojiRasterizer instance, which can draw the emoji onto a
+         *                   Canvas.
          *
          * @return {@link TypefaceEmojiSpan}
          */
         @RequiresApi(19)
         @NonNull
         @Override
-        public EmojiSpan createSpan(@NonNull TypefaceEmojiRasterizer metadata) {
-            return new TypefaceEmojiSpan(metadata);
+        public EmojiSpan createSpan(@NonNull TypefaceEmojiRasterizer rasterizer) {
+            return new TypefaceEmojiSpan(rasterizer);
         }
     }
 
diff --git a/emoji2/emoji2/src/main/java/androidx/emoji2/text/EmojiProcessor.java b/emoji2/emoji2/src/main/java/androidx/emoji2/text/EmojiProcessor.java
index 50cc247..6efcd7a 100644
--- a/emoji2/emoji2/src/main/java/androidx/emoji2/text/EmojiProcessor.java
+++ b/emoji2/emoji2/src/main/java/androidx/emoji2/text/EmojiProcessor.java
@@ -580,20 +580,20 @@
      * @param charSequence the CharSequence that the emoji is in
      * @param start start index of the emoji in the CharSequence
      * @param end end index of the emoji in the CharSequence
-     * @param metadata EmojiMetadata instance for the emoji
+     * @param rasterizer TypefaceEmojiRasterizer instance for the emoji
      *
      * @return {@code true} if the OS can render emoji, {@code false} otherwise
      */
     private boolean hasGlyph(final CharSequence charSequence, int start, final int end,
-            final TypefaceEmojiRasterizer metadata) {
+            final TypefaceEmojiRasterizer rasterizer) {
         // if the existence is not calculated yet
-        if (metadata.getHasGlyph() == TypefaceEmojiRasterizer.HAS_GLYPH_UNKNOWN) {
+        if (rasterizer.getHasGlyph() == TypefaceEmojiRasterizer.HAS_GLYPH_UNKNOWN) {
             final boolean hasGlyph = mGlyphChecker.hasGlyph(charSequence, start, end,
-                    metadata.getSdkAdded());
-            metadata.setHasGlyph(hasGlyph);
+                    rasterizer.getSdkAdded());
+            rasterizer.setHasGlyph(hasGlyph);
         }
 
-        return metadata.getHasGlyph() == TypefaceEmojiRasterizer.HAS_GLYPH_EXISTS;
+        return rasterizer.getHasGlyph() == TypefaceEmojiRasterizer.HAS_GLYPH_EXISTS;
     }
 
     /**
diff --git a/emoji2/emoji2/src/main/java/androidx/emoji2/text/MetadataRepo.java b/emoji2/emoji2/src/main/java/androidx/emoji2/text/MetadataRepo.java
index 50e8495..f08ae06 100644
--- a/emoji2/emoji2/src/main/java/androidx/emoji2/text/MetadataRepo.java
+++ b/emoji2/emoji2/src/main/java/androidx/emoji2/text/MetadataRepo.java
@@ -50,8 +50,9 @@
     private final @NonNull MetadataList mMetadataList;
 
     /**
-     * char presentation of all EmojiMetadata's in a single array. All emojis we have are mapped to
-     * Private Use Area A, in the range U+F0000..U+FFFFD. Therefore each emoji takes 2 chars.
+     * char presentation of all TypefaceEmojiRasterizer's in a single array. All emojis we have are
+     * mapped to Private Use Area A, in the range U+F0000..U+FFFFD. Therefore each emoji takes 2
+     * chars.
      */
     private final @NonNull char[] mEmojiCharArray;
 
@@ -213,7 +214,7 @@
     }
 
     /**
-     * Add an EmojiMetadata to the index.
+     * Add a TypefaceEmojiRasterizer to the index.
      *
      * @hide
      */
@@ -228,8 +229,9 @@
     }
 
     /**
-     * Trie node that holds mapping from emoji codepoint(s) to EmojiMetadata. A single codepoint
-     * emoji is represented by a child of the root node.
+     * Trie node that holds mapping from emoji codepoint(s) to TypefaceEmojiRasterizer.
+     *
+     * A single codepoint emoji is represented by a child of the root node.
      *
      * @hide
      */
diff --git a/emoji2/emoji2/src/main/java/androidx/emoji2/text/TypefaceEmojiRasterizer.java b/emoji2/emoji2/src/main/java/androidx/emoji2/text/TypefaceEmojiRasterizer.java
index f5bbe43..f120b3c 100644
--- a/emoji2/emoji2/src/main/java/androidx/emoji2/text/TypefaceEmojiRasterizer.java
+++ b/emoji2/emoji2/src/main/java/androidx/emoji2/text/TypefaceEmojiRasterizer.java
@@ -89,7 +89,7 @@
     private static final ThreadLocal<MetadataItem> sMetadataItem = new ThreadLocal<>();
 
     /**
-     * Index of the EmojiMetadata in {@link MetadataList}.
+     * Index of the TypefaceEmojiRasterizer in {@link MetadataList}.
      */
     private final int mIndex;
 
@@ -148,7 +148,7 @@
     }
 
     /**
-     * @return a ThreadLocal instance of MetadataItem for this EmojiMetadata
+     * @return a ThreadLocal instance of MetadataItem for this TypefaceEmojiRasterizer
      */
     private MetadataItem getMetadataItem() {
         MetadataItem result = sMetadataItem.get();
@@ -159,10 +159,11 @@
         // MetadataList is a wrapper around the metadata ByteBuffer. MetadataItem is a wrapper with
         // an index (pointer) on this ByteBuffer that represents a single emoji. Both are FlatBuffer
         // classes that wraps a ByteBuffer and gives access to the information in it. In order not
-        // to create a wrapper class for each EmojiMetadata, we use mIndex as the index of the
-        // MetadataItem in the ByteBuffer. We need to reiniitalize the current thread local instance
-        // by executing the statement below. All the statement does is to set an int index in
-        // MetadataItem. the same instance is used by all EmojiMetadata classes in the same thread.
+        // to create a wrapper class for each TypefaceEmojiRasterizer, we use mIndex as the index
+        // of the MetadataItem in the ByteBuffer. We need to reiniitalize the current thread
+        // local instance by executing the statement below. All the statement does is to set an
+        // int index in MetadataItem. the same instance is used by all TypefaceEmojiRasterizer
+        // classes in the same thread.
         mMetadataRepo.getMetadataList().list(result, mIndex);
         return result;
     }
diff --git a/fragment/fragment-lint/src/test/java/androidx/fragment/lint/OnCreateDialogIncorrectCallbackDetectorTest.kt b/fragment/fragment-lint/src/test/java/androidx/fragment/lint/OnCreateDialogIncorrectCallbackDetectorTest.kt
index 9b5246e..0355db2 100644
--- a/fragment/fragment-lint/src/test/java/androidx/fragment/lint/OnCreateDialogIncorrectCallbackDetectorTest.kt
+++ b/fragment/fragment-lint/src/test/java/androidx/fragment/lint/OnCreateDialogIncorrectCallbackDetectorTest.kt
@@ -19,6 +19,7 @@
 import androidx.fragment.lint.stubs.ALERT_DIALOG
 import androidx.fragment.lint.stubs.DIALOG_FRAGMENT
 import com.android.tools.lint.checks.infrastructure.LintDetectorTest
+import com.android.tools.lint.checks.infrastructure.TestMode
 import com.android.tools.lint.detector.api.Detector
 import com.android.tools.lint.detector.api.Issue
 import org.junit.Test
@@ -222,6 +223,7 @@
     fun `kotlin expect fail dialog fragment with cancel listener`() {
         lint().files(dialogFragmentStubKotlinWithCancelListener, DIALOG_FRAGMENT, ALERT_DIALOG)
             .allowCompilationErrors(false)
+            .skipTestModes(TestMode.IMPORT_ALIAS, TestMode.TYPE_ALIAS) // b/266104033
             .run()
             .expect(
                 """
@@ -238,6 +240,7 @@
     fun `kotlin expect fail dialog fragment with dismiss listener`() {
         lint().files(dialogFragmentStubKotlinWithDismissListener, DIALOG_FRAGMENT, ALERT_DIALOG)
             .allowCompilationErrors(false)
+            .skipTestModes(TestMode.IMPORT_ALIAS, TestMode.TYPE_ALIAS) // b/266104033
             .run()
             .expect(
                 """
@@ -257,6 +260,7 @@
             DIALOG_FRAGMENT,
             ALERT_DIALOG
         ).allowCompilationErrors(false)
+            .skipTestModes(TestMode.IMPORT_ALIAS, TestMode.TYPE_ALIAS) // b/266104033
             .run()
             .expect(
                 """
diff --git a/fragment/fragment/build.gradle b/fragment/fragment/build.gradle
index 168db10..5c2b9ed 100644
--- a/fragment/fragment/build.gradle
+++ b/fragment/fragment/build.gradle
@@ -57,6 +57,7 @@
     androidTestImplementation(project(":internal-testutils-runtime"), {
         exclude group: "androidx.fragment", module: "fragment"
     })
+    androidTestImplementation(project(":lifecycle:lifecycle-viewmodel"))
 
     testImplementation(project(":fragment:fragment"))
     testImplementation(libs.kotlinStdlib)
diff --git a/fragment/fragment/src/androidTest/java/androidx/fragment/app/DialogFragmentViewTreeTest.kt b/fragment/fragment/src/androidTest/java/androidx/fragment/app/DialogFragmentViewTreeTest.kt
index d724659..161eeb7 100644
--- a/fragment/fragment/src/androidTest/java/androidx/fragment/app/DialogFragmentViewTreeTest.kt
+++ b/fragment/fragment/src/androidTest/java/androidx/fragment/app/DialogFragmentViewTreeTest.kt
@@ -22,7 +22,7 @@
 import androidx.fragment.app.test.EmptyFragmentTestActivity
 import androidx.fragment.test.R
 import androidx.lifecycle.findViewTreeLifecycleOwner
-import androidx.lifecycle.ViewTreeViewModelStoreOwner
+import androidx.lifecycle.findViewTreeViewModelStoreOwner
 import androidx.savedstate.findViewTreeSavedStateRegistryOwner
 import androidx.test.core.app.ActivityScenario
 import androidx.test.ext.junit.runners.AndroidJUnit4
@@ -57,7 +57,7 @@
                 .that(decorView.findViewTreeLifecycleOwner())
                 .isNotNull()
             assertWithMessage("DialogFragment dialog should have a ViewTreeViewModelStoreOwner")
-                .that(ViewTreeViewModelStoreOwner.get(decorView))
+                .that(decorView.findViewTreeViewModelStoreOwner())
                 .isNotNull()
             assertWithMessage("DialogFragment dialog should have a ViewTreeSavedStateRegistryOwner")
                 .that(decorView.findViewTreeSavedStateRegistryOwner())
diff --git a/fragment/fragment/src/androidTest/java/androidx/fragment/app/FragmentViewLifecycleTest.kt b/fragment/fragment/src/androidTest/java/androidx/fragment/app/FragmentViewLifecycleTest.kt
index dd20ac0..9f4e213 100644
--- a/fragment/fragment/src/androidTest/java/androidx/fragment/app/FragmentViewLifecycleTest.kt
+++ b/fragment/fragment/src/androidTest/java/androidx/fragment/app/FragmentViewLifecycleTest.kt
@@ -29,7 +29,7 @@
 import androidx.lifecycle.MutableLiveData
 import androidx.lifecycle.Observer
 import androidx.lifecycle.findViewTreeLifecycleOwner
-import androidx.lifecycle.ViewTreeViewModelStoreOwner
+import androidx.lifecycle.findViewTreeViewModelStoreOwner
 import androidx.savedstate.findViewTreeSavedStateRegistryOwner
 import androidx.test.annotation.UiThreadTest
 import androidx.test.ext.junit.runners.AndroidJUnit4
@@ -287,7 +287,7 @@
                 observedLifecycleOwner = owner
                 observedTreeLifecycleOwner = fragment.view?.let { it.findViewTreeLifecycleOwner() }
                 observedTreeViewModelStoreOwner = fragment.view?.let {
-                    ViewTreeViewModelStoreOwner.get(it)
+                    it.findViewTreeViewModelStoreOwner()
                 }
                 observedTreeViewSavedStateRegistryOwner = fragment.view?.let {
                     it.findViewTreeSavedStateRegistryOwner()
@@ -308,10 +308,9 @@
                 " after commitNow"
         )
             .that(
-                ViewTreeViewModelStoreOwner.get(
-                    fragment.view
-                        ?: error("no fragment view created")
-                )
+                checkNotNull(fragment.view) {
+                    "no fragment view created"
+                }.findViewTreeViewModelStoreOwner()
             )
             .isSameInstanceAs(fragment.viewLifecycleOwner)
         assertWithMessage(
@@ -415,7 +414,7 @@
 
         override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
             onViewCreatedLifecycleOwner = view.findViewTreeLifecycleOwner()
-            onViewCreatedViewModelStoreOwner = ViewTreeViewModelStoreOwner.get(view)
+            onViewCreatedViewModelStoreOwner = view.findViewTreeViewModelStoreOwner()
             onViewCreatedSavedStateRegistryOwner = view.findViewTreeSavedStateRegistryOwner()
         }
     }
diff --git a/glance/glance-appwidget/src/androidAndroidTest/kotlin/androidx/glance/appwidget/AppWidgetHostTestActivity.kt b/glance/glance-appwidget/src/androidAndroidTest/kotlin/androidx/glance/appwidget/AppWidgetHostTestActivity.kt
index 03f13ef..2b9f6b6 100644
--- a/glance/glance-appwidget/src/androidAndroidTest/kotlin/androidx/glance/appwidget/AppWidgetHostTestActivity.kt
+++ b/glance/glance-appwidget/src/androidAndroidTest/kotlin/androidx/glance/appwidget/AppWidgetHostTestActivity.kt
@@ -176,7 +176,7 @@
             mRemoteViews?.let { return }
             mLatch = CountDownLatch(1)
         }
-        val result = mLatch?.await(5, TimeUnit.SECONDS)!!
+        val result = mLatch?.await(30, TimeUnit.SECONDS)!!
         require(result) { "Timeout before getting RemoteViews" }
     }
 
diff --git a/graphics/filters/filters/build.gradle b/graphics/filters/filters/build.gradle
index e2d3e79..745c9a0 100644
--- a/graphics/filters/filters/build.gradle
+++ b/graphics/filters/filters/build.gradle
@@ -26,19 +26,21 @@
     def media3Version = '1.0.0-beta03'
 
     api(libs.kotlinStdlib)
-    androidTestImplementation(libs.testExtJunit)
-    androidTestImplementation(libs.testCore)
-    androidTestImplementation(libs.testRunner)
-    androidTestImplementation(libs.testRules)
-    androidTestImplementation("androidx.test:core:1.4.0@aar")
 
     // Add dependencies here
+    implementation("androidx.annotation:annotation:1.1.0")
     implementation('androidx.media3:media3-effect:' + media3Version)
     implementation('androidx.media3:media3-common:' + media3Version)
     implementation('androidx.media3:media3-ui:' + media3Version)
     implementation('androidx.media3:media3-exoplayer:' + media3Version)
     implementation('androidx.media3:media3-transformer:' + media3Version)
 
+    // Test dependencies
+    androidTestImplementation(libs.testExtJunit)
+    androidTestImplementation(libs.testCore)
+    androidTestImplementation(libs.testRunner)
+    androidTestImplementation(libs.testRules)
+    androidTestImplementation("androidx.test:core:1.4.0@aar")
 }
 
 android {
@@ -47,16 +49,6 @@
     }
 
     namespace "androidx.graphics.filters"
-    buildFeatures {
-        viewBinding true
-    }
-    compileOptions {
-        sourceCompatibility JavaVersion.VERSION_1_8
-        targetCompatibility JavaVersion.VERSION_1_8
-    }
-    kotlinOptions {
-        jvmTarget = '1.8'
-    }
 }
 
 androidx {
diff --git a/graphics/graphics-core/api/current.txt b/graphics/graphics-core/api/current.txt
index a0cb87a..7f3de7c 100644
--- a/graphics/graphics-core/api/current.txt
+++ b/graphics/graphics-core/api/current.txt
@@ -270,6 +270,7 @@
     method public androidx.graphics.surface.SurfaceControlCompat build();
     method public androidx.graphics.surface.SurfaceControlCompat.Builder setName(String name);
     method public androidx.graphics.surface.SurfaceControlCompat.Builder setParent(android.view.SurfaceView surfaceView);
+    method public androidx.graphics.surface.SurfaceControlCompat.Builder setParent(androidx.graphics.surface.SurfaceControlCompat surfaceControl);
   }
 
   public static final class SurfaceControlCompat.Companion {
diff --git a/graphics/graphics-core/api/public_plus_experimental_current.txt b/graphics/graphics-core/api/public_plus_experimental_current.txt
index a0cb87a..7f3de7c 100644
--- a/graphics/graphics-core/api/public_plus_experimental_current.txt
+++ b/graphics/graphics-core/api/public_plus_experimental_current.txt
@@ -270,6 +270,7 @@
     method public androidx.graphics.surface.SurfaceControlCompat build();
     method public androidx.graphics.surface.SurfaceControlCompat.Builder setName(String name);
     method public androidx.graphics.surface.SurfaceControlCompat.Builder setParent(android.view.SurfaceView surfaceView);
+    method public androidx.graphics.surface.SurfaceControlCompat.Builder setParent(androidx.graphics.surface.SurfaceControlCompat surfaceControl);
   }
 
   public static final class SurfaceControlCompat.Companion {
diff --git a/graphics/graphics-core/api/restricted_current.txt b/graphics/graphics-core/api/restricted_current.txt
index 9a4cd3c..719959c 100644
--- a/graphics/graphics-core/api/restricted_current.txt
+++ b/graphics/graphics-core/api/restricted_current.txt
@@ -271,6 +271,7 @@
     method public androidx.graphics.surface.SurfaceControlCompat build();
     method public androidx.graphics.surface.SurfaceControlCompat.Builder setName(String name);
     method public androidx.graphics.surface.SurfaceControlCompat.Builder setParent(android.view.SurfaceView surfaceView);
+    method public androidx.graphics.surface.SurfaceControlCompat.Builder setParent(androidx.graphics.surface.SurfaceControlCompat surfaceControl);
   }
 
   public static final class SurfaceControlCompat.Companion {
diff --git a/graphics/graphics-core/src/androidTest/java/androidx/graphics/surface/SurfaceControlCompatTest.kt b/graphics/graphics-core/src/androidTest/java/androidx/graphics/surface/SurfaceControlCompatTest.kt
index 2da652a..b696e9f 100644
--- a/graphics/graphics-core/src/androidTest/java/androidx/graphics/surface/SurfaceControlCompatTest.kt
+++ b/graphics/graphics-core/src/androidTest/java/androidx/graphics/surface/SurfaceControlCompatTest.kt
@@ -100,6 +100,42 @@
     }
 
     @Test
+    fun testSurfaceControlCompatBuilder_parentSurfaceControl() {
+        val callbackLatch = CountDownLatch(1)
+        val scenario = ActivityScenario.launch(SurfaceControlWrapperTestActivity::class.java)
+            .moveToState(Lifecycle.State.CREATED)
+
+        try {
+            scenario.onActivity {
+                val callback = object : SurfaceHolderCallback() {
+                    override fun surfaceCreated(sh: SurfaceHolder) {
+                        val parentSc = SurfaceControlCompat.Builder()
+                            .setParent(it.mSurfaceView)
+                            .setName("ParentSurfaceControl")
+                            .build()
+
+                        SurfaceControlCompat.Builder()
+                            .setParent(parentSc)
+                            .setName("ChildSurfaceControl")
+                            .build()
+
+                        callbackLatch.countDown()
+                    }
+                }
+
+                it.addSurface(it.getSurfaceView(), callback)
+            }
+            scenario.moveToState(Lifecycle.State.RESUMED)
+            assertTrue(callbackLatch.await(3000, TimeUnit.MILLISECONDS))
+        } catch (e: java.lang.IllegalArgumentException) {
+            fail()
+        } finally {
+            // ensure activity is destroyed after any failures
+            scenario.moveToState(Lifecycle.State.DESTROYED)
+        }
+    }
+
+    @Test
     fun testSurfaceTransactionCreate() {
         try {
             SurfaceControlCompat.Transaction()
diff --git a/graphics/graphics-core/src/main/java/androidx/graphics/lowlatency/SurfaceViewRenderLayer.kt b/graphics/graphics-core/src/main/java/androidx/graphics/lowlatency/SurfaceViewRenderLayer.kt
index 70ebccf..5355039 100644
--- a/graphics/graphics-core/src/main/java/androidx/graphics/lowlatency/SurfaceViewRenderLayer.kt
+++ b/graphics/graphics-core/src/main/java/androidx/graphics/lowlatency/SurfaceViewRenderLayer.kt
@@ -64,8 +64,8 @@
                 inverse = mBufferTransform.invertBufferTransform(transformHint)
                 mBufferTransform.computeTransform(width, height, inverse)
                 mParentSurfaceControl?.release()
-                mLayerCallback?.onSizeChanged(width, height)
                 mParentSurfaceControl = createDoubleBufferedSurfaceControl()
+                mLayerCallback?.onSizeChanged(width, height)
             }
 
             override fun surfaceDestroyed(p0: SurfaceHolder) {
@@ -90,7 +90,9 @@
     }
 
     override fun setParent(builder: SurfaceControlCompat.Builder) {
-        builder.setParent(surfaceView)
+        mParentSurfaceControl?.let { parentSurfaceControl ->
+            builder.setParent(parentSurfaceControl)
+        }
     }
 
     @RequiresApi(Build.VERSION_CODES.TIRAMISU)
diff --git a/graphics/graphics-core/src/main/java/androidx/graphics/surface/SurfaceControlCompat.kt b/graphics/graphics-core/src/main/java/androidx/graphics/surface/SurfaceControlCompat.kt
index 905db04..9a6fc65 100644
--- a/graphics/graphics-core/src/main/java/androidx/graphics/surface/SurfaceControlCompat.kt
+++ b/graphics/graphics-core/src/main/java/androidx/graphics/surface/SurfaceControlCompat.kt
@@ -134,6 +134,19 @@
         }
 
         /**
+         * Set a parent [SurfaceControlCompat] for the new [SurfaceControlCompat] instance.
+         * Furthermore they stack relatively in Z order, and inherit the transformation of the
+         * parent.
+         * @param surfaceControl Target [SurfaceControlCompat] used as the parent for the newly
+         * created [SurfaceControlCompat] instance
+         */
+        @Suppress("MissingGetterMatchingBuilder")
+        fun setParent(surfaceControl: SurfaceControlCompat): Builder {
+            mBuilderImpl.setParent(surfaceControl)
+            return this
+        }
+
+        /**
          * Set a debugging-name for the [SurfaceControlCompat].
          * @param name Debugging name configured on the [SurfaceControlCompat] instance.
          */
diff --git a/graphics/graphics-core/src/main/java/androidx/graphics/surface/SurfaceControlImpl.kt b/graphics/graphics-core/src/main/java/androidx/graphics/surface/SurfaceControlImpl.kt
index a2ef757..1c7810c 100644
--- a/graphics/graphics-core/src/main/java/androidx/graphics/surface/SurfaceControlImpl.kt
+++ b/graphics/graphics-core/src/main/java/androidx/graphics/surface/SurfaceControlImpl.kt
@@ -67,6 +67,15 @@
         fun setParent(surfaceView: SurfaceView): Builder
 
         /**
+         * Set a parent [SurfaceControlCompat] for the new [SurfaceControlCompat] instance.
+         * Furthermore they stack relatively in Z order, and inherit the transformation of the
+         * parent.
+         * @param surfaceControl Target [SurfaceControlCompat] used as the parent for the newly
+         * created [SurfaceControlCompat] instance
+         */
+        fun setParent(surfaceControl: SurfaceControlCompat): Builder
+
+        /**
          * Set a debugging-name for the [SurfaceControlImpl].
          * @param name Debugging name configured on the [SurfaceControlCompat] instance.
          */
diff --git a/graphics/graphics-core/src/main/java/androidx/graphics/surface/SurfaceControlV29.kt b/graphics/graphics-core/src/main/java/androidx/graphics/surface/SurfaceControlV29.kt
index 481d75a..8605825 100644
--- a/graphics/graphics-core/src/main/java/androidx/graphics/surface/SurfaceControlV29.kt
+++ b/graphics/graphics-core/src/main/java/androidx/graphics/surface/SurfaceControlV29.kt
@@ -68,6 +68,14 @@
         }
 
         /**
+         * See [SurfaceControlWrapper.Builder.setParent]
+         */
+        override fun setParent(surfaceControl: SurfaceControlCompat): SurfaceControlImpl.Builder {
+            builder.setParent(surfaceControl.scImpl.asWrapperSurfaceControl())
+            return this
+        }
+
+        /**
          * See [SurfaceControlWrapper.Builder.setDebugName]
          */
         override fun setName(name: String): SurfaceControlImpl.Builder {
@@ -371,13 +379,6 @@
             )
         }
 
-        private fun SurfaceControlImpl.asWrapperSurfaceControl(): SurfaceControlWrapper =
-            if (this is SurfaceControlV29) {
-                surfaceControl
-            } else {
-                throw IllegalArgumentException("Parent implementation is only for Android T+.")
-            }
-
         private fun SyncFenceImpl.asSyncFenceCompat(): SyncFence =
             if (this is SyncFenceV19) {
                 mSyncFence
@@ -388,4 +389,14 @@
                 )
             }
     }
+
+    private companion object {
+
+        fun SurfaceControlImpl.asWrapperSurfaceControl(): SurfaceControlWrapper =
+            if (this is SurfaceControlV29) {
+                surfaceControl
+            } else {
+                throw IllegalArgumentException("Parent implementation is only for Android T+.")
+            }
+    }
 }
\ No newline at end of file
diff --git a/graphics/graphics-core/src/main/java/androidx/graphics/surface/SurfaceControlV33.kt b/graphics/graphics-core/src/main/java/androidx/graphics/surface/SurfaceControlV33.kt
index ed84d4a..03ecbc1 100644
--- a/graphics/graphics-core/src/main/java/androidx/graphics/surface/SurfaceControlV33.kt
+++ b/graphics/graphics-core/src/main/java/androidx/graphics/surface/SurfaceControlV33.kt
@@ -65,6 +65,14 @@
         }
 
         /**
+         * See [SurfaceControlImpl.Builder.setParent]
+         */
+        override fun setParent(surfaceControl: SurfaceControlCompat): SurfaceControlImpl.Builder {
+            builder.setParent(surfaceControl.scImpl.asFrameworkSurfaceControl())
+            return this
+        }
+
+        /**
          * See [SurfaceControlImpl.Builder.setName]
          */
         override fun setName(name: String): Builder {
@@ -271,13 +279,6 @@
             attachedSurfaceControl.applyTransactionOnDraw(mTransaction)
         }
 
-        private fun SurfaceControlImpl.asFrameworkSurfaceControl(): SurfaceControl =
-            if (this is SurfaceControlV33) {
-                surfaceControl
-            } else {
-                throw IllegalArgumentException("Parent implementation is not for Android T")
-            }
-
         private fun SyncFenceImpl.asSyncFence(): SyncFence =
             if (this is SyncFenceV33) {
                 mSyncFence
@@ -286,4 +287,13 @@
                 IllegalArgumentException("Expected SyncFenceCompat implementation for API level 33")
             }
     }
+
+    private companion object {
+        fun SurfaceControlImpl.asFrameworkSurfaceControl(): SurfaceControl =
+            if (this is SurfaceControlV33) {
+                surfaceControl
+            } else {
+                throw IllegalArgumentException("Parent implementation is not for Android T")
+            }
+    }
 }
\ No newline at end of file
diff --git a/graphics/graphics-core/src/main/java/androidx/graphics/surface/SurfaceControlWrapper.kt b/graphics/graphics-core/src/main/java/androidx/graphics/surface/SurfaceControlWrapper.kt
index 7a5bfab..042a484 100644
--- a/graphics/graphics-core/src/main/java/androidx/graphics/surface/SurfaceControlWrapper.kt
+++ b/graphics/graphics-core/src/main/java/androidx/graphics/surface/SurfaceControlWrapper.kt
@@ -173,20 +173,24 @@
  * initially exposed for SurfaceControl.
  */
 @RequiresApi(Build.VERSION_CODES.Q)
-internal class SurfaceControlWrapper internal constructor(
-    surface: Surface,
-    debugName: String
-) {
-    private var mNativeSurfaceControl: Long = 0
+internal class SurfaceControlWrapper {
 
-    init {
-        mNativeSurfaceControl = JniBindings.nCreateFromSurface(surface, debugName)
-
+    constructor(surfaceControl: SurfaceControlWrapper, debugName: String) {
+        mNativeSurfaceControl = JniBindings.nCreate(surfaceControl.mNativeSurfaceControl, debugName)
         if (mNativeSurfaceControl == 0L) {
             throw IllegalArgumentException()
         }
     }
 
+    constructor(surface: Surface, debugName: String) {
+        mNativeSurfaceControl = JniBindings.nCreateFromSurface(surface, debugName)
+        if (mNativeSurfaceControl == 0L) {
+            throw IllegalArgumentException()
+        }
+    }
+
+    private var mNativeSurfaceControl: Long = 0
+
     /**
      * Compatibility class for ASurfaceTransaction.
      */
@@ -666,11 +670,19 @@
      * Requires a debug name.
      */
     class Builder {
-        private lateinit var mSurface: Surface
+        private var mSurface: Surface? = null
+        private var mSurfaceControl: SurfaceControlWrapper? = null
         private lateinit var mDebugName: String
 
         fun setParent(surface: Surface): Builder {
             mSurface = surface
+            mSurfaceControl = null
+            return this
+        }
+
+        fun setParent(surfaceControlWrapper: SurfaceControlWrapper): Builder {
+            mSurface = null
+            mSurfaceControl = surfaceControlWrapper
             return this
         }
 
@@ -684,7 +696,15 @@
          * Builds the [SurfaceControlWrapper] object
          */
         fun build(): SurfaceControlWrapper {
-            return SurfaceControlWrapper(mSurface, mDebugName)
+            val surface = mSurface
+            val surfaceControl = mSurfaceControl
+            return if (surface != null) {
+                SurfaceControlWrapper(surface, mDebugName)
+            } else if (surfaceControl != null) {
+                SurfaceControlWrapper(surfaceControl, mDebugName)
+            } else {
+                throw IllegalStateException("")
+            }
         }
     }
 }
\ No newline at end of file
diff --git a/health/connect/connect-client/src/main/java/androidx/health/platform/client/request/RegisterForDataNotificationsRequest.kt b/health/connect/connect-client/src/main/java/androidx/health/platform/client/request/RegisterForDataNotificationsRequest.kt
index 1624b3d..3e810cd 100644
--- a/health/connect/connect-client/src/main/java/androidx/health/platform/client/request/RegisterForDataNotificationsRequest.kt
+++ b/health/connect/connect-client/src/main/java/androidx/health/platform/client/request/RegisterForDataNotificationsRequest.kt
@@ -20,6 +20,7 @@
 import androidx.health.platform.client.impl.data.ProtoParcelable
 import androidx.health.platform.client.proto.RequestProto
 
+/** @suppress */
 class RegisterForDataNotificationsRequest(
     override val proto: RequestProto.RegisterForDataNotificationsRequest
 ) : ProtoParcelable<RequestProto.RegisterForDataNotificationsRequest>() {
diff --git a/health/connect/connect-client/src/main/java/androidx/health/platform/client/request/UnregisterFromDataNotificationsRequest.kt b/health/connect/connect-client/src/main/java/androidx/health/platform/client/request/UnregisterFromDataNotificationsRequest.kt
index f0bcc07..4422d71 100644
--- a/health/connect/connect-client/src/main/java/androidx/health/platform/client/request/UnregisterFromDataNotificationsRequest.kt
+++ b/health/connect/connect-client/src/main/java/androidx/health/platform/client/request/UnregisterFromDataNotificationsRequest.kt
@@ -20,6 +20,7 @@
 import androidx.health.platform.client.impl.data.ProtoParcelable
 import androidx.health.platform.client.proto.RequestProto
 
+/** @suppress */
 class UnregisterFromDataNotificationsRequest(
     override val proto: RequestProto.UnregisterFromDataNotificationsRequest
 ) : ProtoParcelable<RequestProto.UnregisterFromDataNotificationsRequest>() {
diff --git a/libraryversions.toml b/libraryversions.toml
index 46498e4..baa15a0 100644
--- a/libraryversions.toml
+++ b/libraryversions.toml
@@ -49,7 +49,7 @@
 DATASTORE_KMP = "1.1.0-dev01"
 DOCUMENTFILE = "1.1.0-alpha02"
 DRAGANDDROP = "1.1.0-alpha01"
-DRAWERLAYOUT = "1.2.0-alpha02"
+DRAWERLAYOUT = "1.2.0-beta01"
 DYNAMICANIMATION = "1.1.0-alpha04"
 DYNAMICANIMATION_KTX = "1.0.0-alpha04"
 EMOJI = "1.2.0-alpha03"
@@ -94,6 +94,7 @@
 PERCENTLAYOUT = "1.1.0-alpha01"
 PREFERENCE = "1.3.0-alpha01"
 PRINT = "1.1.0-beta01"
+PRIVACYSANDBOX_ADS = "1.0.0-alpha01"
 PRIVACYSANDBOX_SDKRUNTIME = "1.0.0-alpha01"
 PRIVACYSANDBOX_TOOLS = "1.0.0-alpha03"
 PRIVACYSANDBOX_UI = "1.0.0-alpha01"
@@ -228,6 +229,7 @@
 PERCENTLAYOUT = { group = "androidx.percentlayout", atomicGroupVersion = "versions.PERCENTLAYOUT" }
 PREFERENCE = { group = "androidx.preference", atomicGroupVersion = "versions.PREFERENCE" }
 PRINT = { group = "androidx.print", atomicGroupVersion = "versions.PRINT" }
+PRIVACYSANDBOX_ADS = { group = "androidx.privacysandbox.ads", atomicGroupVersion = "versions.PRIVACYSANDBOX_ADS" }
 PRIVACYSANDBOX_SDKRUNTIME = { group = "androidx.privacysandbox.sdkruntime", atomicGroupVersion = "versions.PRIVACYSANDBOX_SDKRUNTIME" }
 PRIVACYSANDBOX_TOOLS = { group = "androidx.privacysandbox.tools", atomicGroupVersion = "versions.PRIVACYSANDBOX_TOOLS" }
 PRIVACYSANDBOX_UI = { group = "androidx.privacysandbox.ui", atomicGroupVersion = "versions.PRIVACYSANDBOX_UI" }
diff --git a/lifecycle/lifecycle-livedata-core/api/current.txt b/lifecycle/lifecycle-livedata-core/api/current.txt
index f4df726..f528b4e 100644
--- a/lifecycle/lifecycle-livedata-core/api/current.txt
+++ b/lifecycle/lifecycle-livedata-core/api/current.txt
@@ -25,8 +25,8 @@
     method public void setValue(T!);
   }
 
-  public interface Observer<T> {
-    method public void onChanged(T!);
+  public fun interface Observer<T> {
+    method public void onChanged(T? value);
   }
 
 }
diff --git a/lifecycle/lifecycle-livedata-core/api/public_plus_experimental_current.txt b/lifecycle/lifecycle-livedata-core/api/public_plus_experimental_current.txt
index f4df726..f528b4e 100644
--- a/lifecycle/lifecycle-livedata-core/api/public_plus_experimental_current.txt
+++ b/lifecycle/lifecycle-livedata-core/api/public_plus_experimental_current.txt
@@ -25,8 +25,8 @@
     method public void setValue(T!);
   }
 
-  public interface Observer<T> {
-    method public void onChanged(T!);
+  public fun interface Observer<T> {
+    method public void onChanged(T? value);
   }
 
 }
diff --git a/lifecycle/lifecycle-livedata-core/api/restricted_current.txt b/lifecycle/lifecycle-livedata-core/api/restricted_current.txt
index f4df726..f528b4e 100644
--- a/lifecycle/lifecycle-livedata-core/api/restricted_current.txt
+++ b/lifecycle/lifecycle-livedata-core/api/restricted_current.txt
@@ -25,8 +25,8 @@
     method public void setValue(T!);
   }
 
-  public interface Observer<T> {
-    method public void onChanged(T!);
+  public fun interface Observer<T> {
+    method public void onChanged(T? value);
   }
 
 }
diff --git a/lifecycle/lifecycle-livedata-core/build.gradle b/lifecycle/lifecycle-livedata-core/build.gradle
index be5361f..fba1c86 100644
--- a/lifecycle/lifecycle-livedata-core/build.gradle
+++ b/lifecycle/lifecycle-livedata-core/build.gradle
@@ -19,9 +19,11 @@
 plugins {
     id("AndroidXPlugin")
     id("com.android.library")
+    id("org.jetbrains.kotlin.android")
 }
 
 dependencies {
+    api(libs.kotlinStdlib)
     implementation("androidx.arch.core:core-common:2.1.0")
     implementation("androidx.arch.core:core-runtime:2.1.0")
     api(project(":lifecycle:lifecycle-common"))
diff --git a/lifecycle/lifecycle-livedata-core/src/main/java/androidx/lifecycle/Observer.java b/lifecycle/lifecycle-livedata-core/src/main/java/androidx/lifecycle/Observer.kt
similarity index 72%
rename from lifecycle/lifecycle-livedata-core/src/main/java/androidx/lifecycle/Observer.java
rename to lifecycle/lifecycle-livedata-core/src/main/java/androidx/lifecycle/Observer.kt
index 30afeedf..2df6295 100644
--- a/lifecycle/lifecycle-livedata-core/src/main/java/androidx/lifecycle/Observer.java
+++ b/lifecycle/lifecycle-livedata-core/src/main/java/androidx/lifecycle/Observer.kt
@@ -13,20 +13,17 @@
  * See the License for the specific language governing permissions and
  * limitations under the License.
  */
-
-package androidx.lifecycle;
+package androidx.lifecycle
 
 /**
- * A simple callback that can receive from {@link LiveData}.
- *
- * @param <T> The type of the parameter
+ * A simple callback that can receive from [LiveData].
  *
  * @see LiveData LiveData - for a usage description.
- */
-public interface Observer<T> {
+*/
+fun interface Observer<T> {
+
     /**
-     * Called when the data is changed.
-     * @param t  The new data
+     * Called when the data is changed is changed to [value].
      */
-    void onChanged(T t);
-}
+    fun onChanged(value: T)
+}
\ No newline at end of file
diff --git a/lifecycle/lifecycle-livedata-core/src/test/java/androidx/lifecycle/LiveDataTest.java b/lifecycle/lifecycle-livedata-core/src/test/java/androidx/lifecycle/LiveDataTest.java
index 85834a3..874db97 100644
--- a/lifecycle/lifecycle-livedata-core/src/test/java/androidx/lifecycle/LiveDataTest.java
+++ b/lifecycle/lifecycle-livedata-core/src/test/java/androidx/lifecycle/LiveDataTest.java
@@ -954,7 +954,7 @@
 
     private class FailReentranceObserver<T> implements Observer<T> {
         @Override
-        public void onChanged(@Nullable T t) {
+        public void onChanged(@Nullable T value) {
             assertThat(mInObserver, is(false));
         }
     }
diff --git a/lifecycle/lifecycle-livedata-ktx/src/test/java/androidx/lifecycle/ScopesRule.kt b/lifecycle/lifecycle-livedata-ktx/src/test/java/androidx/lifecycle/ScopesRule.kt
index 7791be2..d1193af 100644
--- a/lifecycle/lifecycle-livedata-ktx/src/test/java/androidx/lifecycle/ScopesRule.kt
+++ b/lifecycle/lifecycle-livedata-ktx/src/test/java/androidx/lifecycle/ScopesRule.kt
@@ -104,8 +104,8 @@
     private val scopes: ScopesRule
 ) : Observer<T> {
     private var items = mutableListOf<T>()
-    override fun onChanged(t: T) {
-        items.add(t)
+    override fun onChanged(value: T) {
+        items.add(value)
     }
 
     fun assertItems(vararg expected: T) {
diff --git a/lifecycle/lifecycle-livedata/src/main/java/androidx/lifecycle/Transformations.kt b/lifecycle/lifecycle-livedata/src/main/java/androidx/lifecycle/Transformations.kt
index 7c8af62..26d7c36 100644
--- a/lifecycle/lifecycle-livedata/src/main/java/androidx/lifecycle/Transformations.kt
+++ b/lifecycle/lifecycle-livedata/src/main/java/androidx/lifecycle/Transformations.kt
@@ -120,8 +120,8 @@
     result.addSource(this, object : Observer<X> {
         var liveData: LiveData<Y>? = null
 
-        override fun onChanged(x: X) {
-            val newLiveData = transform(x)
+        override fun onChanged(value: X) {
+            val newLiveData = transform(value)
             if (liveData === newLiveData) {
                 return
             }
@@ -149,8 +149,8 @@
     result.addSource(this, object : Observer<X> {
         var liveData: LiveData<Y>? = null
 
-        override fun onChanged(x: X) {
-            val newLiveData = switchMapFunction.apply(x)
+        override fun onChanged(value: X) {
+            val newLiveData = switchMapFunction.apply(value)
             if (liveData === newLiveData) {
                 return
             }
@@ -180,14 +180,14 @@
     outputLiveData.addSource(this, object : Observer<X> {
         var firstTime = true
 
-        override fun onChanged(currentValue: X) {
+        override fun onChanged(value: X) {
             val previousValue = outputLiveData.value
             if (firstTime ||
-                previousValue == null && currentValue != null ||
-                previousValue != null && previousValue != currentValue
+                previousValue == null && value != null ||
+                previousValue != null && previousValue != value
             ) {
                 firstTime = false
-                outputLiveData.value = currentValue
+                outputLiveData.value = value
             }
         }
     })
diff --git a/lifecycle/lifecycle-livedata/src/test/java/androidx/lifecycle/TransformationsTest.java b/lifecycle/lifecycle-livedata/src/test/java/androidx/lifecycle/TransformationsTest.java
index e77c136..4b1a493 100644
--- a/lifecycle/lifecycle-livedata/src/test/java/androidx/lifecycle/TransformationsTest.java
+++ b/lifecycle/lifecycle-livedata/src/test/java/androidx/lifecycle/TransformationsTest.java
@@ -238,7 +238,7 @@
         int mTimesUpdated;
 
         @Override
-        public void onChanged(@Nullable T t) {
+        public void onChanged(@Nullable T value) {
             ++mTimesUpdated;
         }
     }
diff --git a/lifecycle/lifecycle-process/src/main/java/androidx/lifecycle/ProcessLifecycleOwner.kt b/lifecycle/lifecycle-process/src/main/java/androidx/lifecycle/ProcessLifecycleOwner.kt
index 657b9e0..9bd8d89 100644
--- a/lifecycle/lifecycle-process/src/main/java/androidx/lifecycle/ProcessLifecycleOwner.kt
+++ b/lifecycle/lifecycle-process/src/main/java/androidx/lifecycle/ProcessLifecycleOwner.kt
@@ -24,6 +24,7 @@
 import androidx.annotation.DoNotInline
 import androidx.annotation.RequiresApi
 import androidx.annotation.VisibleForTesting
+import androidx.lifecycle.ReportFragment.Companion.reportFragment
 
 /**
  * Class that provides lifecycle for the whole application process.
@@ -170,7 +171,7 @@
                 // onActivityPostStarted and onActivityPostResumed callbacks registered in
                 // onActivityPreCreated()
                 if (Build.VERSION.SDK_INT < 29) {
-                    ReportFragment.get(activity).setProcessListener(initializationListener)
+                    activity.reportFragment.setProcessListener(initializationListener)
                 }
             }
 
diff --git a/lifecycle/lifecycle-reactivestreams/src/main/java/androidx/lifecycle/LiveDataReactiveStreams.kt b/lifecycle/lifecycle-reactivestreams/src/main/java/androidx/lifecycle/LiveDataReactiveStreams.kt
index dbf4021..5f09eb2 100644
--- a/lifecycle/lifecycle-reactivestreams/src/main/java/androidx/lifecycle/LiveDataReactiveStreams.kt
+++ b/lifecycle/lifecycle-reactivestreams/src/main/java/androidx/lifecycle/LiveDataReactiveStreams.kt
@@ -75,7 +75,7 @@
         val subscriber: Subscriber<in T>,
         val lifecycle: LifecycleOwner,
         val liveData: LiveData<T>
-    ) : Subscription, Observer<T> {
+    ) : Subscription, Observer<T?> {
         @Volatile
         var canceled = false
 
@@ -86,18 +86,18 @@
         // used on main thread only
         var latest: T? = null
 
-        override fun onChanged(t: T?) {
+        override fun onChanged(value: T?) {
             if (canceled) {
                 return
             }
             if (requested > 0) {
                 latest = null
-                subscriber.onNext(t)
+                subscriber.onNext(value)
                 if (requested != Long.MAX_VALUE) {
                     requested--
                 }
             } else {
-                latest = t
+                latest = value
             }
         }
 
diff --git a/lifecycle/lifecycle-runtime/api/restricted_current.txt b/lifecycle/lifecycle-runtime/api/restricted_current.txt
index fbb9120..704cdb4 100644
--- a/lifecycle/lifecycle-runtime/api/restricted_current.txt
+++ b/lifecycle/lifecycle-runtime/api/restricted_current.txt
@@ -26,13 +26,27 @@
 
   @RestrictTo(androidx.annotation.RestrictTo.Scope.LIBRARY_GROUP_PREFIX) public class ReportFragment extends android.app.Fragment {
     ctor public ReportFragment();
-    method public static void injectIfNeededIn(android.app.Activity!);
-    method public void onActivityCreated(android.os.Bundle!);
+    method public static final androidx.lifecycle.ReportFragment get(android.app.Activity);
+    method public static final void injectIfNeededIn(android.app.Activity activity);
+    method public void onActivityCreated(android.os.Bundle? savedInstanceState);
     method public void onDestroy();
     method public void onPause();
     method public void onResume();
     method public void onStart();
     method public void onStop();
+    method public final void setProcessListener(androidx.lifecycle.ReportFragment.ActivityInitializationListener? processListener);
+    field public static final androidx.lifecycle.ReportFragment.Companion Companion;
+  }
+
+  public static interface ReportFragment.ActivityInitializationListener {
+    method public void onCreate();
+    method public void onResume();
+    method public void onStart();
+  }
+
+  public static final class ReportFragment.Companion {
+    method public androidx.lifecycle.ReportFragment get(android.app.Activity);
+    method public void injectIfNeededIn(android.app.Activity activity);
   }
 
   public final class ViewTreeLifecycleOwner {
diff --git a/lifecycle/lifecycle-runtime/src/main/java/androidx/lifecycle/ReportFragment.java b/lifecycle/lifecycle-runtime/src/main/java/androidx/lifecycle/ReportFragment.java
deleted file mode 100644
index a876708..0000000
--- a/lifecycle/lifecycle-runtime/src/main/java/androidx/lifecycle/ReportFragment.java
+++ /dev/null
@@ -1,231 +0,0 @@
-/*
- * 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 android.app.Activity;
-import android.app.Application;
-import android.os.Build;
-import android.os.Bundle;
-
-import androidx.annotation.NonNull;
-import androidx.annotation.Nullable;
-import androidx.annotation.RequiresApi;
-import androidx.annotation.RestrictTo;
-
-/**
- * Internal class that dispatches initialization events.
- *
- * @hide
- */
-@SuppressWarnings({"UnknownNullness", "deprecation"})
-// TODO https://issuetracker.google.com/issues/112197238
-@RestrictTo(RestrictTo.Scope.LIBRARY_GROUP_PREFIX)
-public class ReportFragment extends android.app.Fragment {
-    private static final String REPORT_FRAGMENT_TAG = "androidx.lifecycle"
-            + ".LifecycleDispatcher.report_fragment_tag";
-
-    public static void injectIfNeededIn(Activity activity) {
-        if (Build.VERSION.SDK_INT >= 29) {
-            // On API 29+, we can register for the correct Lifecycle callbacks directly
-            LifecycleCallbacks.registerIn(activity);
-        }
-        // Prior to API 29 and to maintain compatibility with older versions of
-        // ProcessLifecycleOwner (which may not be updated when lifecycle-runtime is updated and
-        // need to support activities that don't extend from FragmentActivity from support lib),
-        // use a framework fragment to get the correct timing of Lifecycle events
-        android.app.FragmentManager manager = activity.getFragmentManager();
-        if (manager.findFragmentByTag(REPORT_FRAGMENT_TAG) == null) {
-            manager.beginTransaction().add(new ReportFragment(), REPORT_FRAGMENT_TAG).commit();
-            // Hopefully, we are the first to make a transaction.
-            manager.executePendingTransactions();
-        }
-    }
-
-    @SuppressWarnings("deprecation")
-    static void dispatch(@NonNull Activity activity, @NonNull Lifecycle.Event event) {
-        if (activity instanceof LifecycleRegistryOwner) {
-            ((LifecycleRegistryOwner) activity).getLifecycle().handleLifecycleEvent(event);
-            return;
-        }
-
-        if (activity instanceof LifecycleOwner) {
-            Lifecycle lifecycle = ((LifecycleOwner) activity).getLifecycle();
-            if (lifecycle instanceof LifecycleRegistry) {
-                ((LifecycleRegistry) lifecycle).handleLifecycleEvent(event);
-            }
-        }
-    }
-
-    static ReportFragment get(Activity activity) {
-        return (ReportFragment) activity.getFragmentManager().findFragmentByTag(
-                REPORT_FRAGMENT_TAG);
-    }
-
-    private ActivityInitializationListener mProcessListener;
-
-    private void dispatchCreate(ActivityInitializationListener listener) {
-        if (listener != null) {
-            listener.onCreate();
-        }
-    }
-
-    private void dispatchStart(ActivityInitializationListener listener) {
-        if (listener != null) {
-            listener.onStart();
-        }
-    }
-
-    private void dispatchResume(ActivityInitializationListener listener) {
-        if (listener != null) {
-            listener.onResume();
-        }
-    }
-
-    @Override
-    public void onActivityCreated(Bundle savedInstanceState) {
-        super.onActivityCreated(savedInstanceState);
-        dispatchCreate(mProcessListener);
-        dispatch(Lifecycle.Event.ON_CREATE);
-    }
-
-    @Override
-    public void onStart() {
-        super.onStart();
-        dispatchStart(mProcessListener);
-        dispatch(Lifecycle.Event.ON_START);
-    }
-
-    @Override
-    public void onResume() {
-        super.onResume();
-        dispatchResume(mProcessListener);
-        dispatch(Lifecycle.Event.ON_RESUME);
-    }
-
-    @Override
-    public void onPause() {
-        super.onPause();
-        dispatch(Lifecycle.Event.ON_PAUSE);
-    }
-
-    @Override
-    public void onStop() {
-        super.onStop();
-        dispatch(Lifecycle.Event.ON_STOP);
-    }
-
-    @Override
-    public void onDestroy() {
-        super.onDestroy();
-        dispatch(Lifecycle.Event.ON_DESTROY);
-        // just want to be sure that we won't leak reference to an activity
-        mProcessListener = null;
-    }
-
-    private void dispatch(@NonNull Lifecycle.Event event) {
-        if (Build.VERSION.SDK_INT < 29) {
-            // Only dispatch events from ReportFragment on API levels prior
-            // to API 29. On API 29+, this is handled by the ActivityLifecycleCallbacks
-            // added in ReportFragment.injectIfNeededIn
-            dispatch(getActivity(), event);
-        }
-    }
-
-    void setProcessListener(ActivityInitializationListener processListener) {
-        mProcessListener = processListener;
-    }
-
-    interface ActivityInitializationListener {
-        void onCreate();
-
-        void onStart();
-
-        void onResume();
-    }
-
-    // this class isn't inlined only because we need to add a proguard rule for it (b/142778206)
-    // In addition to that registerIn method allows to avoid class verification failure,
-    // because registerActivityLifecycleCallbacks is available only since api 29.
-    @RequiresApi(29)
-    static class LifecycleCallbacks implements Application.ActivityLifecycleCallbacks {
-
-        static void registerIn(Activity activity) {
-            activity.registerActivityLifecycleCallbacks(new LifecycleCallbacks());
-        }
-
-        @Override
-        public void onActivityCreated(@NonNull Activity activity,
-                @Nullable Bundle bundle) {
-        }
-
-        @Override
-        public void onActivityPostCreated(@NonNull Activity activity,
-                @Nullable Bundle savedInstanceState) {
-            dispatch(activity, Lifecycle.Event.ON_CREATE);
-        }
-
-        @Override
-        public void onActivityStarted(@NonNull Activity activity) {
-        }
-
-        @Override
-        public void onActivityPostStarted(@NonNull Activity activity) {
-            dispatch(activity, Lifecycle.Event.ON_START);
-        }
-
-        @Override
-        public void onActivityResumed(@NonNull Activity activity) {
-        }
-
-        @Override
-        public void onActivityPostResumed(@NonNull Activity activity) {
-            dispatch(activity, Lifecycle.Event.ON_RESUME);
-        }
-
-        @Override
-        public void onActivityPrePaused(@NonNull Activity activity) {
-            dispatch(activity, Lifecycle.Event.ON_PAUSE);
-        }
-
-        @Override
-        public void onActivityPaused(@NonNull Activity activity) {
-        }
-
-        @Override
-        public void onActivityPreStopped(@NonNull Activity activity) {
-            dispatch(activity, Lifecycle.Event.ON_STOP);
-        }
-
-        @Override
-        public void onActivityStopped(@NonNull Activity activity) {
-        }
-
-        @Override
-        public void onActivitySaveInstanceState(@NonNull Activity activity,
-                @NonNull Bundle bundle) {
-        }
-
-        @Override
-        public void onActivityPreDestroyed(@NonNull Activity activity) {
-            dispatch(activity, Lifecycle.Event.ON_DESTROY);
-        }
-
-        @Override
-        public void onActivityDestroyed(@NonNull Activity activity) {
-        }
-    }
-}
diff --git a/lifecycle/lifecycle-runtime/src/main/java/androidx/lifecycle/ReportFragment.kt b/lifecycle/lifecycle-runtime/src/main/java/androidx/lifecycle/ReportFragment.kt
new file mode 100644
index 0000000..a7ae3f40
--- /dev/null
+++ b/lifecycle/lifecycle-runtime/src/main/java/androidx/lifecycle/ReportFragment.kt
@@ -0,0 +1,206 @@
+/*
+ * 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 android.app.Activity
+import android.app.Application
+import android.os.Build
+import android.os.Bundle
+import androidx.annotation.RequiresApi
+import androidx.annotation.RestrictTo
+
+/**
+ * Internal class that dispatches initialization events.
+ *
+ * @hide
+ */
+@Suppress("DEPRECATION")
+@RestrictTo(RestrictTo.Scope.LIBRARY_GROUP_PREFIX)
+open class ReportFragment() : android.app.Fragment() {
+    private var processListener: ActivityInitializationListener? = null
+
+    private fun dispatchCreate(listener: ActivityInitializationListener?) {
+        listener?.onCreate()
+    }
+
+    private fun dispatchStart(listener: ActivityInitializationListener?) {
+        listener?.onStart()
+    }
+
+    private fun dispatchResume(listener: ActivityInitializationListener?) {
+        listener?.onResume()
+    }
+
+    override fun onActivityCreated(savedInstanceState: Bundle?) {
+        super.onActivityCreated(savedInstanceState)
+        dispatchCreate(processListener)
+        dispatch(Lifecycle.Event.ON_CREATE)
+    }
+
+    override fun onStart() {
+        super.onStart()
+        dispatchStart(processListener)
+        dispatch(Lifecycle.Event.ON_START)
+    }
+
+    override fun onResume() {
+        super.onResume()
+        dispatchResume(processListener)
+        dispatch(Lifecycle.Event.ON_RESUME)
+    }
+
+    override fun onPause() {
+        super.onPause()
+        dispatch(Lifecycle.Event.ON_PAUSE)
+    }
+
+    override fun onStop() {
+        super.onStop()
+        dispatch(Lifecycle.Event.ON_STOP)
+    }
+
+    override fun onDestroy() {
+        super.onDestroy()
+        dispatch(Lifecycle.Event.ON_DESTROY)
+        // just want to be sure that we won't leak reference to an activity
+        processListener = null
+    }
+
+    private fun dispatch(event: Lifecycle.Event) {
+        if (Build.VERSION.SDK_INT < 29) {
+            // Only dispatch events from ReportFragment on API levels prior
+            // to API 29. On API 29+, this is handled by the ActivityLifecycleCallbacks
+            // added in ReportFragment.injectIfNeededIn
+            dispatch(activity, event)
+        }
+    }
+
+    fun setProcessListener(processListener: ActivityInitializationListener?) {
+        this.processListener = processListener
+    }
+
+    interface ActivityInitializationListener {
+        fun onCreate()
+        fun onStart()
+        fun onResume()
+    }
+
+    // this class isn't inlined only because we need to add a proguard rule for it (b/142778206)
+    // In addition to that registerIn method allows to avoid class verification failure,
+    // because registerActivityLifecycleCallbacks is available only since api 29.
+    @RequiresApi(29)
+    internal class LifecycleCallbacks : Application.ActivityLifecycleCallbacks {
+        override fun onActivityCreated(
+            activity: Activity,
+            bundle: Bundle?
+        ) {}
+
+        override fun onActivityPostCreated(
+            activity: Activity,
+            savedInstanceState: Bundle?
+        ) {
+            dispatch(activity, Lifecycle.Event.ON_CREATE)
+        }
+
+        override fun onActivityStarted(activity: Activity) {}
+
+        override fun onActivityPostStarted(activity: Activity) {
+            dispatch(activity, Lifecycle.Event.ON_START)
+        }
+
+        override fun onActivityResumed(activity: Activity) {}
+
+        override fun onActivityPostResumed(activity: Activity) {
+            dispatch(activity, Lifecycle.Event.ON_RESUME)
+        }
+
+        override fun onActivityPrePaused(activity: Activity) {
+            dispatch(activity, Lifecycle.Event.ON_PAUSE)
+        }
+
+        override fun onActivityPaused(activity: Activity) {}
+
+        override fun onActivityPreStopped(activity: Activity) {
+            dispatch(activity, Lifecycle.Event.ON_STOP)
+        }
+
+        override fun onActivityStopped(activity: Activity) {}
+
+        override fun onActivitySaveInstanceState(
+            activity: Activity,
+            bundle: Bundle
+        ) {}
+
+        override fun onActivityPreDestroyed(activity: Activity) {
+            dispatch(activity, Lifecycle.Event.ON_DESTROY)
+        }
+
+        override fun onActivityDestroyed(activity: Activity) {}
+
+        companion object {
+            @JvmStatic
+            fun registerIn(activity: Activity) {
+                activity.registerActivityLifecycleCallbacks(LifecycleCallbacks())
+            }
+        }
+    }
+
+    companion object {
+        private const val REPORT_FRAGMENT_TAG =
+            "androidx.lifecycle.LifecycleDispatcher.report_fragment_tag"
+
+        @JvmStatic
+        fun injectIfNeededIn(activity: Activity) {
+            if (Build.VERSION.SDK_INT >= 29) {
+                // On API 29+, we can register for the correct Lifecycle callbacks directly
+                LifecycleCallbacks.registerIn(activity)
+            }
+            // Prior to API 29 and to maintain compatibility with older versions of
+            // ProcessLifecycleOwner (which may not be updated when lifecycle-runtime is updated and
+            // need to support activities that don't extend from FragmentActivity from support lib),
+            // use a framework fragment to get the correct timing of Lifecycle events
+            val manager = activity.fragmentManager
+            if (manager.findFragmentByTag(REPORT_FRAGMENT_TAG) == null) {
+                manager.beginTransaction().add(ReportFragment(), REPORT_FRAGMENT_TAG).commit()
+                // Hopefully, we are the first to make a transaction.
+                manager.executePendingTransactions()
+            }
+        }
+
+        @JvmStatic
+        internal fun dispatch(activity: Activity, event: Lifecycle.Event) {
+            if (activity is LifecycleRegistryOwner) {
+                activity.lifecycle.handleLifecycleEvent(event)
+                return
+            }
+            if (activity is LifecycleOwner) {
+                val lifecycle = (activity as LifecycleOwner).lifecycle
+                if (lifecycle is LifecycleRegistry) {
+                    lifecycle.handleLifecycleEvent(event)
+                }
+            }
+        }
+
+        @JvmStatic
+        @get:JvmName("get")
+        val Activity.reportFragment: ReportFragment
+            get() {
+                return this.fragmentManager.findFragmentByTag(
+                    REPORT_FRAGMENT_TAG
+                ) as ReportFragment
+            }
+    }
+}
\ No newline at end of file
diff --git a/lifecycle/lifecycle-viewmodel-compose/src/main/java/androidx/lifecycle/viewmodel/compose/LocalViewModelStoreOwner.kt b/lifecycle/lifecycle-viewmodel-compose/src/main/java/androidx/lifecycle/viewmodel/compose/LocalViewModelStoreOwner.kt
index 4ec4434..a0d4c81 100644
--- a/lifecycle/lifecycle-viewmodel-compose/src/main/java/androidx/lifecycle/viewmodel/compose/LocalViewModelStoreOwner.kt
+++ b/lifecycle/lifecycle-viewmodel-compose/src/main/java/androidx/lifecycle/viewmodel/compose/LocalViewModelStoreOwner.kt
@@ -22,7 +22,7 @@
 import androidx.compose.runtime.compositionLocalOf
 import androidx.compose.ui.platform.LocalView
 import androidx.lifecycle.ViewModelStoreOwner
-import androidx.lifecycle.ViewTreeViewModelStoreOwner
+import androidx.lifecycle.findViewTreeViewModelStoreOwner
 
 /**
  * The CompositionLocal containing the current [ViewModelStoreOwner].
@@ -33,13 +33,13 @@
 
     /**
      * Returns current composition local value for the owner or `null` if one has not
-     * been provided nor is one available via [ViewTreeViewModelStoreOwner.get] on the
+     * been provided nor is one available via [findViewTreeViewModelStoreOwner] on the
      * current [LocalView].
      */
     public val current: ViewModelStoreOwner?
         @Composable
         get() = LocalViewModelStoreOwner.current
-            ?: ViewTreeViewModelStoreOwner.get(LocalView.current)
+            ?: LocalView.current.findViewTreeViewModelStoreOwner()
 
     /**
      * Associates a [LocalViewModelStoreOwner] key to a value in a call to
diff --git a/lifecycle/lifecycle-viewmodel-savedstate/src/main/java/androidx/lifecycle/AbstractSavedStateViewModelFactory.kt b/lifecycle/lifecycle-viewmodel-savedstate/src/main/java/androidx/lifecycle/AbstractSavedStateViewModelFactory.kt
index 6561594..3e09793 100644
--- a/lifecycle/lifecycle-viewmodel-savedstate/src/main/java/androidx/lifecycle/AbstractSavedStateViewModelFactory.kt
+++ b/lifecycle/lifecycle-viewmodel-savedstate/src/main/java/androidx/lifecycle/AbstractSavedStateViewModelFactory.kt
@@ -93,7 +93,7 @@
 
     private fun <T : ViewModel> create(key: String, modelClass: Class<T>): T {
         val controller = LegacySavedStateHandleController
-            .create(savedStateRegistry, lifecycle, key, defaultArgs)
+            .create(savedStateRegistry!!, lifecycle!!, key, defaultArgs)
         val viewModel = create(key, modelClass, controller.handle)
         viewModel.setTagIfAbsent(TAG_SAVED_STATE_HANDLE_CONTROLLER, controller)
         return viewModel
@@ -148,7 +148,7 @@
     override fun onRequery(viewModel: ViewModel) {
         // is need only for legacy path
         if (savedStateRegistry != null) {
-            attachHandleIfNeeded(viewModel, savedStateRegistry, lifecycle)
+            attachHandleIfNeeded(viewModel, savedStateRegistry!!, lifecycle!!)
         }
     }
 
diff --git a/lifecycle/lifecycle-viewmodel-savedstate/src/main/java/androidx/lifecycle/LegacySavedStateHandleController.java b/lifecycle/lifecycle-viewmodel-savedstate/src/main/java/androidx/lifecycle/LegacySavedStateHandleController.java
deleted file mode 100644
index f7f9033..0000000
--- a/lifecycle/lifecycle-viewmodel-savedstate/src/main/java/androidx/lifecycle/LegacySavedStateHandleController.java
+++ /dev/null
@@ -1,92 +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.lifecycle;
-
-import static androidx.lifecycle.Lifecycle.State.INITIALIZED;
-import static androidx.lifecycle.Lifecycle.State.STARTED;
-
-import android.os.Bundle;
-
-import androidx.annotation.NonNull;
-import androidx.savedstate.SavedStateRegistry;
-import androidx.savedstate.SavedStateRegistryOwner;
-
-class LegacySavedStateHandleController {
-
-    private LegacySavedStateHandleController() {}
-
-    static final String TAG_SAVED_STATE_HANDLE_CONTROLLER = "androidx.lifecycle.savedstate.vm.tag";
-
-    static SavedStateHandleController create(SavedStateRegistry registry, Lifecycle lifecycle,
-            String key, Bundle defaultArgs) {
-        Bundle restoredState = registry.consumeRestoredStateForKey(key);
-        SavedStateHandle handle = SavedStateHandle.createHandle(restoredState, defaultArgs);
-        SavedStateHandleController controller = new SavedStateHandleController(key, handle);
-        controller.attachToLifecycle(registry, lifecycle);
-        tryToAddRecreator(registry, lifecycle);
-        return controller;
-    }
-
-    static final class OnRecreation implements SavedStateRegistry.AutoRecreated {
-
-        @Override
-        public void onRecreated(@NonNull SavedStateRegistryOwner owner) {
-            if (!(owner instanceof ViewModelStoreOwner)) {
-                throw new IllegalStateException(
-                        "Internal error: OnRecreation should be registered only on components"
-                                + " that implement ViewModelStoreOwner");
-            }
-            ViewModelStore viewModelStore = ((ViewModelStoreOwner) owner).getViewModelStore();
-            SavedStateRegistry savedStateRegistry = owner.getSavedStateRegistry();
-            for (String key : viewModelStore.keys()) {
-                ViewModel viewModel = viewModelStore.get(key);
-                attachHandleIfNeeded(viewModel, savedStateRegistry, owner.getLifecycle());
-            }
-            if (!viewModelStore.keys().isEmpty()) {
-                savedStateRegistry.runOnNextRecreation(OnRecreation.class);
-            }
-        }
-    }
-
-    static void attachHandleIfNeeded(ViewModel viewModel, SavedStateRegistry registry,
-            Lifecycle lifecycle) {
-        SavedStateHandleController controller = viewModel.getTag(
-                TAG_SAVED_STATE_HANDLE_CONTROLLER);
-        if (controller != null && !controller.isAttached()) {
-            controller.attachToLifecycle(registry, lifecycle);
-            tryToAddRecreator(registry, lifecycle);
-        }
-    }
-
-    private static void tryToAddRecreator(SavedStateRegistry registry, Lifecycle lifecycle) {
-        Lifecycle.State currentState = lifecycle.getCurrentState();
-        if (currentState == INITIALIZED || currentState.isAtLeast(STARTED)) {
-            registry.runOnNextRecreation(OnRecreation.class);
-        } else {
-            lifecycle.addObserver(new LifecycleEventObserver() {
-                @Override
-                public void onStateChanged(@NonNull LifecycleOwner source,
-                        @NonNull Lifecycle.Event event) {
-                    if (event == Lifecycle.Event.ON_START) {
-                        lifecycle.removeObserver(this);
-                        registry.runOnNextRecreation(OnRecreation.class);
-                    }
-                }
-            });
-        }
-    }
-}
diff --git a/lifecycle/lifecycle-viewmodel-savedstate/src/main/java/androidx/lifecycle/LegacySavedStateHandleController.kt b/lifecycle/lifecycle-viewmodel-savedstate/src/main/java/androidx/lifecycle/LegacySavedStateHandleController.kt
new file mode 100644
index 0000000..2f3de01
--- /dev/null
+++ b/lifecycle/lifecycle-viewmodel-savedstate/src/main/java/androidx/lifecycle/LegacySavedStateHandleController.kt
@@ -0,0 +1,93 @@
+/*
+ * 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.lifecycle
+
+import android.os.Bundle
+import androidx.lifecycle.SavedStateHandle.Companion.createHandle
+import androidx.savedstate.SavedStateRegistry
+import androidx.savedstate.SavedStateRegistryOwner
+
+internal object LegacySavedStateHandleController {
+    const val TAG_SAVED_STATE_HANDLE_CONTROLLER = "androidx.lifecycle.savedstate.vm.tag"
+
+    @JvmStatic
+    fun create(
+        registry: SavedStateRegistry,
+        lifecycle: Lifecycle,
+        key: String?,
+        defaultArgs: Bundle?
+    ): SavedStateHandleController {
+        val restoredState = registry.consumeRestoredStateForKey(key!!)
+        val handle = createHandle(restoredState, defaultArgs)
+        val controller = SavedStateHandleController(key, handle)
+        controller.attachToLifecycle(registry, lifecycle)
+        tryToAddRecreator(registry, lifecycle)
+        return controller
+    }
+
+    @JvmStatic
+    fun attachHandleIfNeeded(
+        viewModel: ViewModel,
+        registry: SavedStateRegistry,
+        lifecycle: Lifecycle
+    ) {
+        val controller = viewModel.getTag<SavedStateHandleController>(
+            TAG_SAVED_STATE_HANDLE_CONTROLLER
+        )
+        if (controller != null && !controller.isAttached) {
+            controller.attachToLifecycle(registry, lifecycle)
+            tryToAddRecreator(registry, lifecycle)
+        }
+    }
+
+    private fun tryToAddRecreator(registry: SavedStateRegistry, lifecycle: Lifecycle) {
+        val currentState = lifecycle.currentState
+        if (currentState === Lifecycle.State.INITIALIZED ||
+            currentState.isAtLeast(Lifecycle.State.STARTED)) {
+            registry.runOnNextRecreation(OnRecreation::class.java)
+        } else {
+            lifecycle.addObserver(object : LifecycleEventObserver {
+                override fun onStateChanged(
+                    source: LifecycleOwner,
+                    event: Lifecycle.Event
+                ) {
+                    if (event === Lifecycle.Event.ON_START) {
+                        lifecycle.removeObserver(this)
+                        registry.runOnNextRecreation(OnRecreation::class.java)
+                    }
+                }
+            })
+        }
+    }
+
+    internal class OnRecreation : SavedStateRegistry.AutoRecreated {
+        override fun onRecreated(owner: SavedStateRegistryOwner) {
+            check(owner is ViewModelStoreOwner) {
+                ("Internal error: OnRecreation should be registered only on components " +
+                    "that implement ViewModelStoreOwner")
+            }
+            val viewModelStore = (owner as ViewModelStoreOwner).viewModelStore
+            val savedStateRegistry = owner.savedStateRegistry
+            for (key in viewModelStore.keys()) {
+                val viewModel = viewModelStore[key]
+                attachHandleIfNeeded(viewModel!!, savedStateRegistry, owner.lifecycle)
+            }
+            if (viewModelStore.keys().isNotEmpty()) {
+                savedStateRegistry.runOnNextRecreation(OnRecreation::class.java)
+            }
+        }
+    }
+}
\ No newline at end of file
diff --git a/lifecycle/lifecycle-viewmodel-savedstate/src/main/java/androidx/lifecycle/SavedStateViewModelFactory.kt b/lifecycle/lifecycle-viewmodel-savedstate/src/main/java/androidx/lifecycle/SavedStateViewModelFactory.kt
index 2e22611..aa24f21 100644
--- a/lifecycle/lifecycle-viewmodel-savedstate/src/main/java/androidx/lifecycle/SavedStateViewModelFactory.kt
+++ b/lifecycle/lifecycle-viewmodel-savedstate/src/main/java/androidx/lifecycle/SavedStateViewModelFactory.kt
@@ -156,12 +156,11 @@
      */
     fun <T : ViewModel> create(key: String, modelClass: Class<T>): T {
         // empty constructor was called.
-        if (lifecycle == null) {
-            throw UnsupportedOperationException(
+        val lifecycle = lifecycle
+            ?: throw UnsupportedOperationException(
                 "SavedStateViewModelFactory constructed with empty constructor supports only " +
                     "calls to create(modelClass: Class<T>, extras: CreationExtras)."
             )
-        }
         val isAndroidViewModel = AndroidViewModel::class.java.isAssignableFrom(modelClass)
         val constructor: Constructor<T>? = if (isAndroidViewModel && application != null) {
             findMatchingConstructor(modelClass, ANDROID_VIEWMODEL_SIGNATURE)
@@ -169,14 +168,13 @@
             findMatchingConstructor(modelClass, VIEWMODEL_SIGNATURE)
         }
         // doesn't need SavedStateHandle
-        if (constructor == null) {
-            // If you are using a stateful constructor and no application is available, we
+        constructor
+            ?: // If you are using a stateful constructor and no application is available, we
             // use an instance factory instead.
             return if (application != null) factory.create(modelClass)
-                else instance.create(modelClass)
-        }
+            else instance.create(modelClass)
         val controller = LegacySavedStateHandleController.create(
-            savedStateRegistry, lifecycle, key, defaultArgs
+            savedStateRegistry!!, lifecycle, key, defaultArgs
         )
         val viewModel: T = if (isAndroidViewModel && application != null) {
             newInstance(modelClass, constructor, application!!, controller.handle)
@@ -212,8 +210,8 @@
         if (lifecycle != null) {
             LegacySavedStateHandleController.attachHandleIfNeeded(
                 viewModel,
-                savedStateRegistry,
-                lifecycle
+                savedStateRegistry!!,
+                lifecycle!!
             )
         }
     }
diff --git a/lifecycle/lifecycle-viewmodel/api/current.txt b/lifecycle/lifecycle-viewmodel/api/current.txt
index 577a5be..75944f4 100644
--- a/lifecycle/lifecycle-viewmodel/api/current.txt
+++ b/lifecycle/lifecycle-viewmodel/api/current.txt
@@ -83,12 +83,12 @@
   }
 
   public final class ViewTreeViewModelKt {
-    method public static androidx.lifecycle.ViewModelStoreOwner? findViewTreeViewModelStoreOwner(android.view.View);
+    method @Deprecated public static androidx.lifecycle.ViewModelStoreOwner? findViewTreeViewModelStoreOwner(android.view.View view);
   }
 
-  public class ViewTreeViewModelStoreOwner {
+  public final class ViewTreeViewModelStoreOwner {
     method public static androidx.lifecycle.ViewModelStoreOwner? get(android.view.View);
-    method public static void set(android.view.View, androidx.lifecycle.ViewModelStoreOwner?);
+    method public static void set(android.view.View, androidx.lifecycle.ViewModelStoreOwner? viewModelStoreOwner);
   }
 
 }
diff --git a/lifecycle/lifecycle-viewmodel/api/public_plus_experimental_current.txt b/lifecycle/lifecycle-viewmodel/api/public_plus_experimental_current.txt
index 577a5be..75944f4 100644
--- a/lifecycle/lifecycle-viewmodel/api/public_plus_experimental_current.txt
+++ b/lifecycle/lifecycle-viewmodel/api/public_plus_experimental_current.txt
@@ -83,12 +83,12 @@
   }
 
   public final class ViewTreeViewModelKt {
-    method public static androidx.lifecycle.ViewModelStoreOwner? findViewTreeViewModelStoreOwner(android.view.View);
+    method @Deprecated public static androidx.lifecycle.ViewModelStoreOwner? findViewTreeViewModelStoreOwner(android.view.View view);
   }
 
-  public class ViewTreeViewModelStoreOwner {
+  public final class ViewTreeViewModelStoreOwner {
     method public static androidx.lifecycle.ViewModelStoreOwner? get(android.view.View);
-    method public static void set(android.view.View, androidx.lifecycle.ViewModelStoreOwner?);
+    method public static void set(android.view.View, androidx.lifecycle.ViewModelStoreOwner? viewModelStoreOwner);
   }
 
 }
diff --git a/lifecycle/lifecycle-viewmodel/api/restricted_current.txt b/lifecycle/lifecycle-viewmodel/api/restricted_current.txt
index 577a5be..75944f4 100644
--- a/lifecycle/lifecycle-viewmodel/api/restricted_current.txt
+++ b/lifecycle/lifecycle-viewmodel/api/restricted_current.txt
@@ -83,12 +83,12 @@
   }
 
   public final class ViewTreeViewModelKt {
-    method public static androidx.lifecycle.ViewModelStoreOwner? findViewTreeViewModelStoreOwner(android.view.View);
+    method @Deprecated public static androidx.lifecycle.ViewModelStoreOwner? findViewTreeViewModelStoreOwner(android.view.View view);
   }
 
-  public class ViewTreeViewModelStoreOwner {
+  public final class ViewTreeViewModelStoreOwner {
     method public static androidx.lifecycle.ViewModelStoreOwner? get(android.view.View);
-    method public static void set(android.view.View, androidx.lifecycle.ViewModelStoreOwner?);
+    method public static void set(android.view.View, androidx.lifecycle.ViewModelStoreOwner? viewModelStoreOwner);
   }
 
 }
diff --git a/lifecycle/lifecycle-viewmodel/src/androidTest/java/androidx/lifecycle/ViewTreeViewModelStoreOwnerTest.kt b/lifecycle/lifecycle-viewmodel/src/androidTest/java/androidx/lifecycle/ViewTreeViewModelStoreOwnerTest.kt
index f466eef..2fd80c7 100644
--- a/lifecycle/lifecycle-viewmodel/src/androidTest/java/androidx/lifecycle/ViewTreeViewModelStoreOwnerTest.kt
+++ b/lifecycle/lifecycle-viewmodel/src/androidTest/java/androidx/lifecycle/ViewTreeViewModelStoreOwnerTest.kt
@@ -36,19 +36,19 @@
         val v = View(InstrumentationRegistry.getInstrumentation().context)
 
         assertWithMessage("initial ViewModelStoreOwner expects null")
-            .that(ViewTreeViewModelStoreOwner.get(v))
+            .that(v.findViewTreeViewModelStoreOwner())
             .isNull()
 
         val fakeOwner: ViewModelStoreOwner = FakeViewModelStoreOwner()
-        ViewTreeViewModelStoreOwner.set(v, fakeOwner)
+        v.setViewTreeViewModelStoreOwner(fakeOwner)
 
         assertWithMessage("get the ViewModelStoreOwner set directly")
-            .that(ViewTreeViewModelStoreOwner.get(v))
+            .that(v.findViewTreeViewModelStoreOwner())
             .isEqualTo(fakeOwner)
     }
 
     /**
-     * minimal test that checks View.findViewTreeViewModelStoreOwner works
+     * minimal test that checks View..findViewTreeViewModelStoreOwner works
      */
     @Test
     fun setFindsSameView() {
@@ -59,7 +59,7 @@
             .isNull()
 
         val fakeOwner: ViewModelStoreOwner = FakeViewModelStoreOwner()
-        ViewTreeViewModelStoreOwner.set(v, fakeOwner)
+        v.setViewTreeViewModelStoreOwner(fakeOwner)
 
         assertWithMessage("get the ViewModelStoreOwner set directly")
             .that(v.findViewTreeViewModelStoreOwner())
@@ -80,20 +80,20 @@
         parent.addView(child)
 
         assertWithMessage("initial ViewModelStoreOwner expects null")
-            .that(ViewTreeViewModelStoreOwner.get(child))
+            .that(child.findViewTreeViewModelStoreOwner())
             .isNull()
 
         val fakeOwner: ViewModelStoreOwner = FakeViewModelStoreOwner()
-        ViewTreeViewModelStoreOwner.set(root, fakeOwner)
+        root.setViewTreeViewModelStoreOwner(fakeOwner)
 
         assertWithMessage("root sees owner")
-            .that(ViewTreeViewModelStoreOwner.get(root))
+            .that(root.findViewTreeViewModelStoreOwner())
             .isEqualTo(fakeOwner)
         assertWithMessage("direct child sees owner")
-            .that(ViewTreeViewModelStoreOwner.get(parent))
+            .that(parent.findViewTreeViewModelStoreOwner())
             .isEqualTo(fakeOwner)
         assertWithMessage("grandchild sees owner")
-            .that(ViewTreeViewModelStoreOwner.get(child))
+            .that(child.findViewTreeViewModelStoreOwner())
             .isEqualTo(fakeOwner)
     }
 
@@ -112,23 +112,23 @@
         parent.addView(child)
 
         assertWithMessage("initial ViewModelStoreOwner expects null")
-            .that(ViewTreeViewModelStoreOwner.get(child))
+            .that(child.findViewTreeViewModelStoreOwner())
             .isNull()
 
         val rootFakeOwner: ViewModelStoreOwner = FakeViewModelStoreOwner()
-        ViewTreeViewModelStoreOwner.set(root, rootFakeOwner)
+        root.setViewTreeViewModelStoreOwner(rootFakeOwner)
 
         val parentFakeOwner: ViewModelStoreOwner = FakeViewModelStoreOwner()
-        ViewTreeViewModelStoreOwner.set(parent, parentFakeOwner)
+        parent.setViewTreeViewModelStoreOwner(parentFakeOwner)
 
         assertWithMessage("root sees owner")
-            .that(ViewTreeViewModelStoreOwner.get(root))
+            .that(root.findViewTreeViewModelStoreOwner())
             .isEqualTo(rootFakeOwner)
         assertWithMessage("direct child sees owner")
-            .that(ViewTreeViewModelStoreOwner.get(parent))
+            .that(parent.findViewTreeViewModelStoreOwner())
             .isEqualTo(parentFakeOwner)
         assertWithMessage("grandchild sees owner")
-            .that(ViewTreeViewModelStoreOwner.get(child))
+            .that(child.findViewTreeViewModelStoreOwner())
             .isEqualTo(parentFakeOwner)
     }
 
diff --git a/lifecycle/lifecycle-viewmodel/src/main/java/androidx/lifecycle/ViewTreeViewModel.kt b/lifecycle/lifecycle-viewmodel/src/main/java/androidx/lifecycle/ViewTreeViewModel.kt
index 48a3877b..9d49d055 100644
--- a/lifecycle/lifecycle-viewmodel/src/main/java/androidx/lifecycle/ViewTreeViewModel.kt
+++ b/lifecycle/lifecycle-viewmodel/src/main/java/androidx/lifecycle/ViewTreeViewModel.kt
@@ -1,5 +1,5 @@
 /*
- * Copyright 2020 The Android Open Source Project
+ * Copyright 2023 The Android Open Source Project
  *
  * Licensed under the Apache License, Version 2.0 (the "License");
  * you may not use this file except in compliance with the License.
@@ -22,5 +22,13 @@
  * Locates the [ViewModelStoreOwner] associated with this [View], if present.
  * This may be used to retain state associated with this view across configuration changes.
  */
-public fun View.findViewTreeViewModelStoreOwner(): ViewModelStoreOwner? =
-    ViewTreeViewModelStoreOwner.get(this)
+@Deprecated(
+    message = "Replaced by View.findViewTreeViewModelStoreOwner in ViewTreeViewModelStoreOwner",
+    replaceWith = ReplaceWith(
+        "View.findViewTreeViewModelStoreOwner",
+        "androidx.lifecycle.ViewTreeViewModelStoreOwner"
+    ),
+    level = DeprecationLevel.HIDDEN
+)
+public fun findViewTreeViewModelStoreOwner(view: View): ViewModelStoreOwner? =
+    view.findViewTreeViewModelStoreOwner()
diff --git a/lifecycle/lifecycle-viewmodel/src/main/java/androidx/lifecycle/ViewTreeViewModelStoreOwner.java b/lifecycle/lifecycle-viewmodel/src/main/java/androidx/lifecycle/ViewTreeViewModelStoreOwner.java
deleted file mode 100644
index ccf8720..0000000
--- a/lifecycle/lifecycle-viewmodel/src/main/java/androidx/lifecycle/ViewTreeViewModelStoreOwner.java
+++ /dev/null
@@ -1,74 +0,0 @@
-/*
- * Copyright 2020 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- *      http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-package androidx.lifecycle;
-
-import android.view.View;
-import android.view.ViewParent;
-
-import androidx.annotation.NonNull;
-import androidx.annotation.Nullable;
-import androidx.lifecycle.viewmodel.R;
-
-/**
- * Accessors for finding a view tree-local {@link ViewModelStoreOwner} that allows access to a
- * {@link ViewModelStore} for the given view.
- */
-public class ViewTreeViewModelStoreOwner {
-    private ViewTreeViewModelStoreOwner() {
-        // No instances
-    }
-
-    /**
-     * Set the {@link ViewModelStoreOwner} associated with the given {@link View}.
-     * Calls to {@link #get(View)} from this view or descendants will return
-     * {@code viewModelStoreOwner}.
-     *
-     * <p>This should only be called by constructs such as activities or fragments that manage
-     * a view tree and retain state through a {@link ViewModelStoreOwner}. Callers
-     * should only set a {@link ViewModelStoreOwner} that will be <em>stable.</em> The associated
-     * {@link ViewModelStore} should be cleared if the view tree is removed and is not
-     * guaranteed to later become reattached to a window.</p>
-     *
-     * @param view Root view associated with the viewModelStoreOwner
-     * @param viewModelStoreOwner ViewModelStoreOwner associated with the given view
-     */
-    public static void set(@NonNull View view, @Nullable ViewModelStoreOwner viewModelStoreOwner) {
-        view.setTag(R.id.view_tree_view_model_store_owner, viewModelStoreOwner);
-    }
-
-    /**
-     * Retrieve the {@link ViewModelStoreOwner} associated with the given {@link View}.
-     * This may be used to retain state associated with this view across configuration changes.
-     *
-     * @param view View to fetch a {@link ViewModelStoreOwner} for
-     * @return The {@link ViewModelStoreOwner} associated with this view and/or some subset
-     *         of its ancestors
-     */
-    @Nullable
-    public static ViewModelStoreOwner get(@NonNull View view) {
-        ViewModelStoreOwner found = (ViewModelStoreOwner) view.getTag(
-                R.id.view_tree_view_model_store_owner);
-        if (found != null) return found;
-        ViewParent parent = view.getParent();
-        while (found == null && parent instanceof View) {
-            final View parentView = (View) parent;
-            found = (ViewModelStoreOwner) parentView.getTag(R.id.view_tree_view_model_store_owner);
-            parent = parentView.getParent();
-        }
-        return found;
-    }
-}
diff --git a/lifecycle/lifecycle-viewmodel/src/main/java/androidx/lifecycle/ViewTreeViewModelStoreOwner.kt b/lifecycle/lifecycle-viewmodel/src/main/java/androidx/lifecycle/ViewTreeViewModelStoreOwner.kt
new file mode 100644
index 0000000..7239652
--- /dev/null
+++ b/lifecycle/lifecycle-viewmodel/src/main/java/androidx/lifecycle/ViewTreeViewModelStoreOwner.kt
@@ -0,0 +1,55 @@
+/*
+ * 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.
+ */
+@file:JvmName("ViewTreeViewModelStoreOwner")
+
+package androidx.lifecycle
+
+import android.view.View
+import androidx.lifecycle.viewmodel.R
+
+/**
+ * Set the [ViewModelStoreOwner] associated with the given [View].
+ * Calls to [get] from this view or descendants will return
+ * `viewModelStoreOwner`.
+ *
+ * This should only be called by constructs such as activities or fragments that manage
+ * a view tree and retain state through a [ViewModelStoreOwner]. Callers
+ * should only set a [ViewModelStoreOwner] that will be *stable.* The associated
+ * [ViewModelStore] should be cleared if the view tree is removed and is not
+ * guaranteed to later become reattached to a window.
+ *
+ * @param viewModelStoreOwner ViewModelStoreOwner associated with the given view
+ */
+@JvmName("set")
+fun View.setViewTreeViewModelStoreOwner(viewModelStoreOwner: ViewModelStoreOwner?) {
+    setTag(R.id.view_tree_view_model_store_owner, viewModelStoreOwner)
+}
+
+/**
+ * Retrieve the [ViewModelStoreOwner] associated with the given [View].
+ * This may be used to retain state associated with this view across configuration changes.
+ *
+ * @return The [ViewModelStoreOwner] associated with this view and/or some subset
+ * of its ancestors
+ */
+@JvmName("get")
+fun View.findViewTreeViewModelStoreOwner(): ViewModelStoreOwner? {
+    return generateSequence(this) { view ->
+        view.parent as? View
+    }.mapNotNull { view ->
+        view.getTag(R.id.view_tree_view_model_store_owner) as? ViewModelStoreOwner
+    }.firstOrNull()
+}
\ No newline at end of file
diff --git a/lint-checks/src/main/java/androidx/build/lint/TargetApiAnnotationUsageDetector.kt b/lint-checks/src/main/java/androidx/build/lint/TargetApiAnnotationUsageDetector.kt
index 7027b1e..e96d19d9 100644
--- a/lint-checks/src/main/java/androidx/build/lint/TargetApiAnnotationUsageDetector.kt
+++ b/lint-checks/src/main/java/androidx/build/lint/TargetApiAnnotationUsageDetector.kt
@@ -29,6 +29,7 @@
 import com.android.tools.lint.detector.api.Scope
 import com.android.tools.lint.detector.api.Severity
 import org.jetbrains.uast.UAnnotation
+import org.jetbrains.uast.namePsiElement
 
 /**
  * Enforces policy banning use of the `@TargetApi` annotation.
@@ -44,9 +45,17 @@
     private inner class AnnotationChecker(val context: JavaContext) : UElementHandler() {
         override fun visitAnnotation(node: UAnnotation) {
             if (node.qualifiedName == "android.annotation.TargetApi") {
+
+                // To support Kotlin's type aliases, we need to check the pattern against the symbol
+                // instead of a constant ("TargetApi") to pass Lint's IMPORT_ALIAS test mode. In the
+                // case where namePsiElement returns null (which shouldn't happen), fall back to the
+                // RegEx check.
+                val searchPattern = node.namePsiElement?.text
+                    ?: "(?:android\\.annotation\\.)?TargetApi"
+
                 val lintFix = fix().name("Replace with `@RequiresApi`")
                     .replace()
-                    .pattern("(?:android\\.annotation\\.)?TargetApi")
+                    .pattern(searchPattern)
                     .with("androidx.annotation.RequiresApi")
                     .shortenNames()
                     .autoFix(true, true)
diff --git a/lint-checks/src/test/java/androidx/build/lint/TargetApiAnnotationUsageDetectorTest.kt b/lint-checks/src/test/java/androidx/build/lint/TargetApiAnnotationUsageDetectorTest.kt
index 5bf4605..2a77039 100644
--- a/lint-checks/src/test/java/androidx/build/lint/TargetApiAnnotationUsageDetectorTest.kt
+++ b/lint-checks/src/test/java/androidx/build/lint/TargetApiAnnotationUsageDetectorTest.kt
@@ -20,10 +20,9 @@
 
 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.checks.infrastructure.TestLintTask
 import com.android.tools.lint.detector.api.Detector
 import com.android.tools.lint.detector.api.Issue
-
 import org.junit.Test
 
 class TargetApiAnnotationUsageDetectorTest : LintDetectorTest() {
@@ -33,11 +32,11 @@
         TargetApiAnnotationUsageDetector.ISSUE
     )
 
-    private fun check(testFile: TestFile): TestLintResult {
+    private fun checkTask(testFile: TestFile): TestLintTask {
         return lint().files(
             java(annotationSource),
             testFile
-        ).run()
+        )
     }
 
     private val annotationSource = """
@@ -101,7 +100,8 @@
         """.trimIndent()
         /* ktlint-enable max-line-length */
 
-        check(input)
+        checkTask(input)
+            .run()
             .expect(expected)
             .expectFixDiffs(expectFixDiffs)
     }
@@ -149,7 +149,8 @@
         """.trimIndent()
         /* ktlint-enable max-line-length */
 
-        check(input)
+        checkTask(input)
+            .run()
             .expect(expected)
             .expectFixDiffs(expectFixDiffs)
     }
diff --git a/mediarouter/mediarouter/src/main/java/androidx/mediarouter/app/SystemOutputSwitcherDialogController.java b/mediarouter/mediarouter/src/main/java/androidx/mediarouter/app/SystemOutputSwitcherDialogController.java
index ad6bb9d..9782159 100644
--- a/mediarouter/mediarouter/src/main/java/androidx/mediarouter/app/SystemOutputSwitcherDialogController.java
+++ b/mediarouter/mediarouter/src/main/java/androidx/mediarouter/app/SystemOutputSwitcherDialogController.java
@@ -63,6 +63,14 @@
     /**
      * Shows the system output switcher dialog.
      *
+     * <p>The appearance and precise behaviour of the system output switcher dialog
+     * may vary across different devices, OS versions, and form factors,
+     * but the basic functionality stays the same.
+     *
+     * <p>See
+     * <a href="https://developer.android.com/guide/topics/media/media-routing#output-switcher">
+     * Output Switcher documentation</a> for more details.
+     *
      * @param context Android context
      * @return {@code true} if the dialog was shown successfully and {@code false} otherwise
      */
diff --git a/navigation/navigation-common/src/main/java/androidx/navigation/NavigatorState.kt b/navigation/navigation-common/src/main/java/androidx/navigation/NavigatorState.kt
index 889e453..f202e8d 100644
--- a/navigation/navigation-common/src/main/java/androidx/navigation/NavigatorState.kt
+++ b/navigation/navigation-common/src/main/java/androidx/navigation/NavigatorState.kt
@@ -19,6 +19,7 @@
 import android.os.Bundle
 import androidx.annotation.CallSuper
 import androidx.annotation.RestrictTo
+import androidx.lifecycle.Lifecycle
 import kotlinx.coroutines.flow.MutableStateFlow
 import kotlinx.coroutines.flow.StateFlow
 import kotlinx.coroutines.flow.asStateFlow
@@ -78,6 +79,14 @@
      * @see popWithTransition
      */
     public open fun pushWithTransition(backStackEntry: NavBackStackEntry) {
+        // When passed an entry that is already transitioning via a call to push, ignore the call
+        // since we are already moving to the proper state.
+        if (
+            _transitionsInProgress.value.any { it === backStackEntry } &&
+            backStack.value.any { it === backStackEntry }
+        ) {
+            return
+        }
         val previousEntry = backStack.value.lastOrNull()
         // When navigating, we need to mark the outgoing entry as transitioning until it
         // finishes its outgoing animation.
@@ -121,6 +130,14 @@
      * @see pushWithTransition
      */
     public open fun popWithTransition(popUpTo: NavBackStackEntry, saveState: Boolean) {
+        // When passed an entry that is already transitioning via a call to pop, ignore the call
+        // since we are already moving to the proper state.
+        if (
+            _transitionsInProgress.value.any { it === popUpTo } &&
+            backStack.value.none { it === popUpTo }
+        ) {
+            return
+        }
         _transitionsInProgress.value = _transitionsInProgress.value + popUpTo
         val incomingEntry = backStack.value.lastOrNull { entry ->
             entry != popUpTo &&
diff --git a/navigation/navigation-compose-lint/src/test/java/androidx/navigation/compose/lint/Stubs.kt b/navigation/navigation-compose-lint/src/test/java/androidx/navigation/compose/lint/Stubs.kt
index 414792c..6ab5a6b 100644
--- a/navigation/navigation-compose-lint/src/test/java/androidx/navigation/compose/lint/Stubs.kt
+++ b/navigation/navigation-compose-lint/src/test/java/androidx/navigation/compose/lint/Stubs.kt
@@ -16,9 +16,9 @@
 
 package androidx.navigation.compose.lint
 
-import androidx.compose.lint.test.compiledStub
+import androidx.compose.lint.test.bytecodeStub
 
-internal val NAV_BACK_STACK_ENTRY = compiledStub(
+internal val NAV_BACK_STACK_ENTRY = bytecodeStub(
     filename = "NavBackStackEntry.kt",
     filepath = "androidx/navigation",
     checksum = 0x6920c3ac,
@@ -46,7 +46,7 @@
     """
 )
 
-internal val NAV_CONTROLLER = compiledStub(
+internal val NAV_CONTROLLER = bytecodeStub(
     filename = "NavController.kt",
     filepath = "androidx/navigation",
     checksum = 0xa6eda16e,
@@ -81,7 +81,7 @@
     """
 )
 
-internal val NAV_GRAPH_BUILDER = compiledStub(
+internal val NAV_GRAPH_BUILDER = bytecodeStub(
     filename = "NavGraphBuilder.kt",
     filepath = "androidx/navigation",
     checksum = 0xf26bfe8b,
@@ -110,7 +110,7 @@
     """
 )
 
-internal val NAV_GRAPH_COMPOSABLE = compiledStub(
+internal val NAV_GRAPH_COMPOSABLE = bytecodeStub(
     filename = "NavGraphBuilder.kt",
     filepath = "androidx/navigation/compose",
     checksum = 0x6920624a,
@@ -156,7 +156,7 @@
     """
 )
 
-internal val NAV_HOST = compiledStub(
+internal val NAV_HOST = bytecodeStub(
     filename = "NavHost.kt",
     filepath = "androidx/navigation/compose",
     checksum = 0x72aa34d0,
diff --git a/navigation/navigation-dynamic-features-fragment/src/androidTest/java/androidx/navigation/dynamicfeatures/fragment/ui/DefaultProgressFragmentTest.kt b/navigation/navigation-dynamic-features-fragment/src/androidTest/java/androidx/navigation/dynamicfeatures/fragment/ui/DefaultProgressFragmentTest.kt
index 57d781b..99c67e7 100644
--- a/navigation/navigation-dynamic-features-fragment/src/androidTest/java/androidx/navigation/dynamicfeatures/fragment/ui/DefaultProgressFragmentTest.kt
+++ b/navigation/navigation-dynamic-features-fragment/src/androidTest/java/androidx/navigation/dynamicfeatures/fragment/ui/DefaultProgressFragmentTest.kt
@@ -72,8 +72,8 @@
                 // it to fail before we check for test failure.
                 val liveData = viewModel.installMonitor!!.status
                 val observer = object : Observer<SplitInstallSessionState> {
-                    override fun onChanged(state: SplitInstallSessionState) {
-                        if (state.status() == SplitInstallSessionStatus.FAILED) {
+                    override fun onChanged(value: SplitInstallSessionState) {
+                        if (value.status() == SplitInstallSessionStatus.FAILED) {
                             liveData.removeObserver(this)
                             failureCountdownLatch.countDown()
                         }
diff --git a/navigation/navigation-dynamic-features-fragment/src/main/java/androidx/navigation/dynamicfeatures/fragment/ui/AbstractProgressFragment.kt b/navigation/navigation-dynamic-features-fragment/src/main/java/androidx/navigation/dynamicfeatures/fragment/ui/AbstractProgressFragment.kt
index c550abe..e0caf39 100644
--- a/navigation/navigation-dynamic-features-fragment/src/main/java/androidx/navigation/dynamicfeatures/fragment/ui/AbstractProgressFragment.kt
+++ b/navigation/navigation-dynamic-features-fragment/src/main/java/androidx/navigation/dynamicfeatures/fragment/ui/AbstractProgressFragment.kt
@@ -126,59 +126,60 @@
     private inner class StateObserver constructor(private val monitor: DynamicInstallMonitor) :
         Observer<SplitInstallSessionState> {
 
-        override fun onChanged(sessionState: SplitInstallSessionState?) {
-            if (sessionState != null) {
-                if (sessionState.hasTerminalStatus()) {
-                    monitor.status.removeObserver(this)
+        override fun onChanged(
+            @Suppress("PARAMETER_NAME_CHANGED_ON_OVERRIDE")
+            sessionState: SplitInstallSessionState
+        ) {
+            if (sessionState.hasTerminalStatus()) {
+                monitor.status.removeObserver(this)
+            }
+            when (sessionState.status()) {
+                SplitInstallSessionStatus.INSTALLED -> {
+                    onInstalled()
+                    navigate()
                 }
-                when (sessionState.status()) {
-                    SplitInstallSessionStatus.INSTALLED -> {
-                        onInstalled()
-                        navigate()
-                    }
-                    SplitInstallSessionStatus.REQUIRES_USER_CONFIRMATION ->
-                        try {
-                            val splitInstallManager = monitor.splitInstallManager
-                            if (splitInstallManager == null) {
-                                onFailed(SplitInstallErrorCode.INTERNAL_ERROR)
-                                return
-                            }
-                            splitInstallManager.startConfirmationDialogForResult(
-                                sessionState,
-                                IntentSenderForResultStarter { intent,
-                                    _,
-                                    fillInIntent,
-                                    flagsMask,
-                                    flagsValues,
-                                    _,
-                                    _ ->
-                                    intentSenderLauncher.launch(
-                                        IntentSenderRequest.Builder(intent)
-                                            .setFillInIntent(fillInIntent)
-                                            .setFlags(flagsValues, flagsMask)
-                                            .build()
-                                    )
-                                },
-                                INSTALL_REQUEST_CODE
-                            )
-                        } catch (e: IntentSender.SendIntentException) {
+                SplitInstallSessionStatus.REQUIRES_USER_CONFIRMATION ->
+                    try {
+                        val splitInstallManager = monitor.splitInstallManager
+                        if (splitInstallManager == null) {
                             onFailed(SplitInstallErrorCode.INTERNAL_ERROR)
+                            return
                         }
-                    SplitInstallSessionStatus.CANCELED -> onCancelled()
-                    SplitInstallSessionStatus.FAILED -> onFailed(sessionState.errorCode())
-                    SplitInstallSessionStatus.UNKNOWN ->
-                        onFailed(SplitInstallErrorCode.INTERNAL_ERROR)
-                    SplitInstallSessionStatus.CANCELING,
-                    SplitInstallSessionStatus.DOWNLOADED,
-                    SplitInstallSessionStatus.DOWNLOADING,
-                    SplitInstallSessionStatus.INSTALLING,
-                    SplitInstallSessionStatus.PENDING -> {
-                        onProgress(
-                            sessionState.status(),
-                            sessionState.bytesDownloaded(),
-                            sessionState.totalBytesToDownload()
+                        splitInstallManager.startConfirmationDialogForResult(
+                            sessionState,
+                            IntentSenderForResultStarter { intent,
+                                _,
+                                fillInIntent,
+                                flagsMask,
+                                flagsValues,
+                                _,
+                                _ ->
+                                intentSenderLauncher.launch(
+                                    IntentSenderRequest.Builder(intent)
+                                        .setFillInIntent(fillInIntent)
+                                        .setFlags(flagsValues, flagsMask)
+                                        .build()
+                                )
+                            },
+                            INSTALL_REQUEST_CODE
                         )
+                    } catch (e: IntentSender.SendIntentException) {
+                        onFailed(SplitInstallErrorCode.INTERNAL_ERROR)
                     }
+                SplitInstallSessionStatus.CANCELED -> onCancelled()
+                SplitInstallSessionStatus.FAILED -> onFailed(sessionState.errorCode())
+                SplitInstallSessionStatus.UNKNOWN ->
+                    onFailed(SplitInstallErrorCode.INTERNAL_ERROR)
+                SplitInstallSessionStatus.CANCELING,
+                SplitInstallSessionStatus.DOWNLOADED,
+                SplitInstallSessionStatus.DOWNLOADING,
+                SplitInstallSessionStatus.INSTALLING,
+                SplitInstallSessionStatus.PENDING -> {
+                    onProgress(
+                        sessionState.status(),
+                        sessionState.bytesDownloaded(),
+                        sessionState.totalBytesToDownload()
+                    )
                 }
             }
         }
diff --git a/navigation/navigation-safe-args-generator/src/main/kotlin/androidx/navigation/safe/args/generator/kotlin/KotlinTypes.kt b/navigation/navigation-safe-args-generator/src/main/kotlin/androidx/navigation/safe/args/generator/kotlin/KotlinTypes.kt
index 8f35ae4..5a42e74 100644
--- a/navigation/navigation-safe-args-generator/src/main/kotlin/androidx/navigation/safe/args/generator/kotlin/KotlinTypes.kt
+++ b/navigation/navigation-safe-args-generator/src/main/kotlin/androidx/navigation/safe/args/generator/kotlin/KotlinTypes.kt
@@ -184,7 +184,7 @@
     is ObjectArrayType -> builder.apply {
         val baseType = (arg.type.typeName() as ParameterizedTypeName).typeArguments.first()
         addStatement(
-            "%L = %L.get<Array<%T>>(%S)?.map { it as %T }?.toTypedArray()",
+            "%L = %L.get<Array<%T>>(%S)?.map·{ it as %T }?.toTypedArray()",
             lValue, savedStateHandle, PARCELABLE_CLASSNAME, arg.name, baseType
         )
     }
diff --git a/navigation/navigation-safe-args-generator/src/test/kotlin/androidx/navigation/safe/args/generator/KotlinNavWriterTest.kt b/navigation/navigation-safe-args-generator/src/test/kotlin/androidx/navigation/safe/args/generator/KotlinNavWriterTest.kt
index e27fd0c..a434d22 100644
--- a/navigation/navigation-safe-args-generator/src/test/kotlin/androidx/navigation/safe/args/generator/KotlinNavWriterTest.kt
+++ b/navigation/navigation-safe-args-generator/src/test/kotlin/androidx/navigation/safe/args/generator/KotlinNavWriterTest.kt
@@ -232,7 +232,7 @@
                 Argument("referenceZeroDefaultValue", ReferenceType, IntValue("0")),
                 Argument("floatArg", FloatType, FloatValue("1")),
                 Argument("floatArrayArg", FloatArrayType),
-                Argument("objectArrayArg", ObjectArrayType("android.content.pm.ActivityInfo")),
+                Argument("objectArrayArgument", ObjectArrayType("android.content.pm.ActivityInfo")),
                 Argument("boolArg", BoolType, BooleanValue("true")),
                 Argument(
                     "optionalParcelable",
diff --git a/navigation/navigation-safe-args-generator/src/test/test-data/expected/kotlin_nav_writer_test/MainFragmentArgs.kt b/navigation/navigation-safe-args-generator/src/test/test-data/expected/kotlin_nav_writer_test/MainFragmentArgs.kt
index 478ecd3..b8a3e37 100644
--- a/navigation/navigation-safe-args-generator/src/test/test-data/expected/kotlin_nav_writer_test/MainFragmentArgs.kt
+++ b/navigation/navigation-safe-args-generator/src/test/test-data/expected/kotlin_nav_writer_test/MainFragmentArgs.kt
@@ -21,7 +21,7 @@
 public data class MainFragmentArgs(
   public val main: String,
   public val floatArrayArg: FloatArray,
-  public val objectArrayArg: Array<ActivityInfo>,
+  public val objectArrayArgument: Array<ActivityInfo>,
   public val optional: Int = -1,
   public val reference: Int = R.drawable.background,
   public val referenceZeroDefaultValue: Int = 0,
@@ -39,7 +39,7 @@
     result.putInt("referenceZeroDefaultValue", this.referenceZeroDefaultValue)
     result.putFloat("floatArg", this.floatArg)
     result.putFloatArray("floatArrayArg", this.floatArrayArg)
-    result.putParcelableArray("objectArrayArg", this.objectArrayArg)
+    result.putParcelableArray("objectArrayArgument", this.objectArrayArgument)
     result.putBoolean("boolArg", this.boolArg)
     if (Parcelable::class.java.isAssignableFrom(ActivityInfo::class.java)) {
       result.putParcelable("optionalParcelable", this.optionalParcelable as Parcelable?)
@@ -63,7 +63,7 @@
     result.set("referenceZeroDefaultValue", this.referenceZeroDefaultValue)
     result.set("floatArg", this.floatArg)
     result.set("floatArrayArg", this.floatArrayArg)
-    result.set("objectArrayArg", this.objectArrayArg)
+    result.set("objectArrayArgument", this.objectArrayArgument)
     result.set("boolArg", this.boolArg)
     if (Parcelable::class.java.isAssignableFrom(ActivityInfo::class.java)) {
       result.set("optionalParcelable", this.optionalParcelable as Parcelable?)
@@ -125,15 +125,15 @@
       } else {
         throw IllegalArgumentException("Required argument \"floatArrayArg\" is missing and does not have an android:defaultValue")
       }
-      val __objectArrayArg : Array<ActivityInfo>?
-      if (bundle.containsKey("objectArrayArg")) {
-        __objectArrayArg = bundle.getParcelableArray("objectArrayArg")?.map { it as ActivityInfo
-            }?.toTypedArray()
-        if (__objectArrayArg == null) {
-          throw IllegalArgumentException("Argument \"objectArrayArg\" is marked as non-null but was passed a null value.")
+      val __objectArrayArgument : Array<ActivityInfo>?
+      if (bundle.containsKey("objectArrayArgument")) {
+        __objectArrayArgument = bundle.getParcelableArray("objectArrayArgument")?.map { it as
+            ActivityInfo }?.toTypedArray()
+        if (__objectArrayArgument == null) {
+          throw IllegalArgumentException("Argument \"objectArrayArgument\" is marked as non-null but was passed a null value.")
         }
       } else {
-        throw IllegalArgumentException("Required argument \"objectArrayArg\" is missing and does not have an android:defaultValue")
+        throw IllegalArgumentException("Required argument \"objectArrayArgument\" is missing and does not have an android:defaultValue")
       }
       val __boolArg : Boolean
       if (bundle.containsKey("boolArg")) {
@@ -168,8 +168,9 @@
       } else {
         __enumArg = AccessMode.READ
       }
-      return MainFragmentArgs(__main, __floatArrayArg, __objectArrayArg, __optional, __reference,
-          __referenceZeroDefaultValue, __floatArg, __boolArg, __optionalParcelable, __enumArg)
+      return MainFragmentArgs(__main, __floatArrayArg, __objectArrayArgument, __optional,
+          __reference, __referenceZeroDefaultValue, __floatArg, __boolArg, __optionalParcelable,
+          __enumArg)
     }
 
     @JvmStatic
@@ -228,15 +229,16 @@
       } else {
         throw IllegalArgumentException("Required argument \"floatArrayArg\" is missing and does not have an android:defaultValue")
       }
-      val __objectArrayArg : Array<ActivityInfo>?
-      if (savedStateHandle.contains("objectArrayArg")) {
-        __objectArrayArg = savedStateHandle.get<Array<Parcelable>>("objectArrayArg")?.map { it as
-            ActivityInfo }?.toTypedArray()
-        if (__objectArrayArg == null) {
-          throw IllegalArgumentException("Argument \"objectArrayArg\" is marked as non-null but was passed a null value")
+      val __objectArrayArgument : Array<ActivityInfo>?
+      if (savedStateHandle.contains("objectArrayArgument")) {
+        __objectArrayArgument =
+            savedStateHandle.get<Array<Parcelable>>("objectArrayArgument")?.map { it as ActivityInfo
+            }?.toTypedArray()
+        if (__objectArrayArgument == null) {
+          throw IllegalArgumentException("Argument \"objectArrayArgument\" is marked as non-null but was passed a null value")
         }
       } else {
-        throw IllegalArgumentException("Required argument \"objectArrayArg\" is missing and does not have an android:defaultValue")
+        throw IllegalArgumentException("Required argument \"objectArrayArgument\" is missing and does not have an android:defaultValue")
       }
       val __boolArg : Boolean?
       if (savedStateHandle.contains("boolArg")) {
@@ -274,8 +276,9 @@
       } else {
         __enumArg = AccessMode.READ
       }
-      return MainFragmentArgs(__main, __floatArrayArg, __objectArrayArg, __optional, __reference,
-          __referenceZeroDefaultValue, __floatArg, __boolArg, __optionalParcelable, __enumArg)
+      return MainFragmentArgs(__main, __floatArrayArg, __objectArrayArgument, __optional,
+          __reference, __referenceZeroDefaultValue, __floatArg, __boolArg, __optionalParcelable,
+          __enumArg)
     }
   }
 }
diff --git a/navigation/navigation-testing/src/androidTest/java/androidx/navigation/testing/TestNavigatorStateTest.kt b/navigation/navigation-testing/src/androidTest/java/androidx/navigation/testing/TestNavigatorStateTest.kt
index faa7248..12f81ba 100644
--- a/navigation/navigation-testing/src/androidTest/java/androidx/navigation/testing/TestNavigatorStateTest.kt
+++ b/navigation/navigation-testing/src/androidTest/java/androidx/navigation/testing/TestNavigatorStateTest.kt
@@ -228,6 +228,81 @@
         assertThat(viewModel.wasCleared).isFalse()
     }
 
+    @Test
+    fun testTransitionInterruptPushPop() {
+        val navigator = TestTransitionNavigator()
+        navigator.onAttach(state)
+        val firstEntry = state.createBackStackEntry(navigator.createDestination(), null)
+
+        navigator.navigate(listOf(firstEntry), null, null)
+        state.markTransitionComplete(firstEntry)
+
+        val secondEntry = state.createBackStackEntry(navigator.createDestination(), null)
+        navigator.navigate(listOf(secondEntry), null, null)
+        assertThat(state.transitionsInProgress.value.contains(firstEntry)).isTrue()
+        assertThat(state.transitionsInProgress.value.contains(secondEntry)).isTrue()
+        assertThat(firstEntry.lifecycle.currentState)
+            .isEqualTo(Lifecycle.State.CREATED)
+        assertThat(secondEntry.lifecycle.currentState)
+            .isEqualTo(Lifecycle.State.STARTED)
+
+        navigator.popBackStack(secondEntry, true)
+        assertThat(state.transitionsInProgress.value.contains(firstEntry)).isTrue()
+        assertThat(state.transitionsInProgress.value.contains(secondEntry)).isTrue()
+        state.markTransitionComplete(firstEntry)
+        state.markTransitionComplete(secondEntry)
+        assertThat(firstEntry.lifecycle.currentState)
+            .isEqualTo(Lifecycle.State.RESUMED)
+        assertThat(secondEntry.lifecycle.currentState)
+            .isEqualTo(Lifecycle.State.DESTROYED)
+    }
+
+    @Test
+    fun testTransitionInterruptPopPush() {
+        val navigator = TestTransitionNavigator()
+        navigator.onAttach(state)
+        val firstEntry = state.createBackStackEntry(navigator.createDestination(), null)
+
+        navigator.navigate(listOf(firstEntry), null, null)
+        state.markTransitionComplete(firstEntry)
+
+        val secondEntry = state.createBackStackEntry(navigator.createDestination(), null)
+        navigator.navigate(listOf(secondEntry), null, null)
+        assertThat(state.transitionsInProgress.value.contains(firstEntry)).isTrue()
+        assertThat(state.transitionsInProgress.value.contains(secondEntry)).isTrue()
+        assertThat(firstEntry.lifecycle.currentState)
+            .isEqualTo(Lifecycle.State.CREATED)
+        assertThat(secondEntry.lifecycle.currentState)
+            .isEqualTo(Lifecycle.State.STARTED)
+        state.markTransitionComplete(firstEntry)
+        state.markTransitionComplete(secondEntry)
+
+        navigator.popBackStack(secondEntry, true)
+        assertThat(state.transitionsInProgress.value.contains(firstEntry)).isTrue()
+        assertThat(state.transitionsInProgress.value.contains(secondEntry)).isTrue()
+
+        val secondEntryReplace = state.createBackStackEntry(navigator.createDestination(), null)
+        navigator.navigate(listOf(secondEntryReplace), null, null)
+        assertThat(state.transitionsInProgress.value.contains(firstEntry)).isTrue()
+        assertThat(state.transitionsInProgress.value.contains(secondEntry)).isTrue()
+        assertThat(state.transitionsInProgress.value.contains(secondEntryReplace)).isTrue()
+        assertThat(firstEntry.lifecycle.currentState)
+            .isEqualTo(Lifecycle.State.CREATED)
+        assertThat(secondEntry.lifecycle.currentState)
+            .isEqualTo(Lifecycle.State.CREATED)
+        assertThat(secondEntryReplace.lifecycle.currentState)
+            .isEqualTo(Lifecycle.State.STARTED)
+        state.markTransitionComplete(firstEntry)
+        state.markTransitionComplete(secondEntry)
+        state.markTransitionComplete(secondEntryReplace)
+        assertThat(firstEntry.lifecycle.currentState)
+            .isEqualTo(Lifecycle.State.CREATED)
+        assertThat(secondEntry.lifecycle.currentState)
+            .isEqualTo(Lifecycle.State.DESTROYED)
+        assertThat(secondEntryReplace.lifecycle.currentState)
+            .isEqualTo(Lifecycle.State.RESUMED)
+    }
+
     @Navigator.Name("test")
     internal class TestNavigator : Navigator<NavDestination>() {
         override fun createDestination(): NavDestination = NavDestination(this)
diff --git a/privacysandbox/OWNERS b/privacysandbox/OWNERS
index 8615f90..d52e717 100644
--- a/privacysandbox/OWNERS
+++ b/privacysandbox/OWNERS
@@ -2,3 +2,4 @@
 ltenorio@google.com
 nicoroulet@google.com
 akulakov@google.com
+npattan@google.com # For ads
diff --git a/privacysandbox/ads/OWNERS b/privacysandbox/ads/OWNERS
new file mode 100644
index 0000000..67d0de6
--- /dev/null
+++ b/privacysandbox/ads/OWNERS
@@ -0,0 +1,3 @@
+# Please keep this list alphabetically sorted
+jmarkoff@google.com
+npattan@google.com
diff --git a/privacysandbox/ads/ads-adservices-java/api/current.txt b/privacysandbox/ads/ads-adservices-java/api/current.txt
new file mode 100644
index 0000000..26eea8b
--- /dev/null
+++ b/privacysandbox/ads/ads-adservices-java/api/current.txt
@@ -0,0 +1,92 @@
+// Signature format: 4.0
+package androidx.privacysandbox.ads.adservices.java.adid {
+
+  public abstract class AdIdManagerFutures {
+    method public static final androidx.privacysandbox.ads.adservices.java.adid.AdIdManagerFutures? from(android.content.Context context);
+    method @RequiresPermission(android.adservices.common.AdServicesPermissions.ACCESS_ADSERVICES_AD_ID) public abstract com.google.common.util.concurrent.ListenableFuture<androidx.privacysandbox.ads.adservices.adid.AdId> getAdIdAsync();
+    field public static final androidx.privacysandbox.ads.adservices.java.adid.AdIdManagerFutures.Companion Companion;
+  }
+
+  public static final class AdIdManagerFutures.Companion {
+    method public androidx.privacysandbox.ads.adservices.java.adid.AdIdManagerFutures? from(android.content.Context context);
+  }
+
+}
+
+package androidx.privacysandbox.ads.adservices.java.adselection {
+
+  public abstract class AdSelectionManagerFutures {
+    method public static final androidx.privacysandbox.ads.adservices.java.adselection.AdSelectionManagerFutures? from(android.content.Context context);
+    method @RequiresPermission(android.adservices.common.AdServicesPermissions.ACCESS_ADSERVICES_CUSTOM_AUDIENCE) public abstract com.google.common.util.concurrent.ListenableFuture<kotlin.Unit> reportImpressionAsync(androidx.privacysandbox.ads.adservices.adselection.ReportImpressionRequest reportImpressionRequest);
+    method @RequiresPermission(android.adservices.common.AdServicesPermissions.ACCESS_ADSERVICES_CUSTOM_AUDIENCE) public abstract com.google.common.util.concurrent.ListenableFuture<androidx.privacysandbox.ads.adservices.adselection.AdSelectionOutcome> selectAdsAsync(androidx.privacysandbox.ads.adservices.adselection.AdSelectionConfig adSelectionConfig);
+    field public static final androidx.privacysandbox.ads.adservices.java.adselection.AdSelectionManagerFutures.Companion Companion;
+  }
+
+  public static final class AdSelectionManagerFutures.Companion {
+    method public androidx.privacysandbox.ads.adservices.java.adselection.AdSelectionManagerFutures? from(android.content.Context context);
+  }
+
+}
+
+package androidx.privacysandbox.ads.adservices.java.appsetid {
+
+  public abstract class AppSetIdManagerFutures {
+    method public static final androidx.privacysandbox.ads.adservices.java.appsetid.AppSetIdManagerFutures? from(android.content.Context context);
+    method public abstract com.google.common.util.concurrent.ListenableFuture<androidx.privacysandbox.ads.adservices.appsetid.AppSetId> getAppSetIdAsync();
+    field public static final androidx.privacysandbox.ads.adservices.java.appsetid.AppSetIdManagerFutures.Companion Companion;
+  }
+
+  public static final class AppSetIdManagerFutures.Companion {
+    method public androidx.privacysandbox.ads.adservices.java.appsetid.AppSetIdManagerFutures? from(android.content.Context context);
+  }
+
+}
+
+package androidx.privacysandbox.ads.adservices.java.customaudience {
+
+  public abstract class CustomAudienceManagerFutures {
+    method public static final androidx.privacysandbox.ads.adservices.java.customaudience.CustomAudienceManagerFutures? from(android.content.Context context);
+    method @RequiresPermission(android.adservices.common.AdServicesPermissions.ACCESS_ADSERVICES_CUSTOM_AUDIENCE) public abstract com.google.common.util.concurrent.ListenableFuture<kotlin.Unit> joinCustomAudienceAsync(androidx.privacysandbox.ads.adservices.customaudience.JoinCustomAudienceRequest request);
+    method @RequiresPermission(android.adservices.common.AdServicesPermissions.ACCESS_ADSERVICES_CUSTOM_AUDIENCE) public abstract com.google.common.util.concurrent.ListenableFuture<kotlin.Unit> leaveCustomAudienceAsync(androidx.privacysandbox.ads.adservices.customaudience.LeaveCustomAudienceRequest request);
+    field public static final androidx.privacysandbox.ads.adservices.java.customaudience.CustomAudienceManagerFutures.Companion Companion;
+  }
+
+  public static final class CustomAudienceManagerFutures.Companion {
+    method public androidx.privacysandbox.ads.adservices.java.customaudience.CustomAudienceManagerFutures? from(android.content.Context context);
+  }
+
+}
+
+package androidx.privacysandbox.ads.adservices.java.measurement {
+
+  public abstract class MeasurementManagerFutures {
+    method public abstract com.google.common.util.concurrent.ListenableFuture<kotlin.Unit> deleteRegistrationsAsync(androidx.privacysandbox.ads.adservices.measurement.DeletionRequest deletionRequest);
+    method public static final androidx.privacysandbox.ads.adservices.java.measurement.MeasurementManagerFutures? from(android.content.Context context);
+    method @RequiresPermission(android.adservices.common.AdServicesPermissions.ACCESS_ADSERVICES_ATTRIBUTION) public abstract com.google.common.util.concurrent.ListenableFuture<java.lang.Integer> getMeasurementApiStatusAsync();
+    method @RequiresPermission(android.adservices.common.AdServicesPermissions.ACCESS_ADSERVICES_ATTRIBUTION) public abstract com.google.common.util.concurrent.ListenableFuture<kotlin.Unit> registerSourceAsync(android.net.Uri attributionSource, android.view.InputEvent? inputEvent);
+    method @RequiresPermission(android.adservices.common.AdServicesPermissions.ACCESS_ADSERVICES_ATTRIBUTION) public abstract com.google.common.util.concurrent.ListenableFuture<kotlin.Unit> registerTriggerAsync(android.net.Uri trigger);
+    method @RequiresPermission(android.adservices.common.AdServicesPermissions.ACCESS_ADSERVICES_ATTRIBUTION) public abstract com.google.common.util.concurrent.ListenableFuture<kotlin.Unit> registerWebSourceAsync(androidx.privacysandbox.ads.adservices.measurement.WebSourceRegistrationRequest request);
+    method @RequiresPermission(android.adservices.common.AdServicesPermissions.ACCESS_ADSERVICES_ATTRIBUTION) public abstract com.google.common.util.concurrent.ListenableFuture<kotlin.Unit> registerWebTriggerAsync(androidx.privacysandbox.ads.adservices.measurement.WebTriggerRegistrationRequest request);
+    field public static final androidx.privacysandbox.ads.adservices.java.measurement.MeasurementManagerFutures.Companion Companion;
+  }
+
+  public static final class MeasurementManagerFutures.Companion {
+    method public androidx.privacysandbox.ads.adservices.java.measurement.MeasurementManagerFutures? from(android.content.Context context);
+  }
+
+}
+
+package androidx.privacysandbox.ads.adservices.java.topics {
+
+  public abstract class TopicsManagerFutures {
+    method public static final androidx.privacysandbox.ads.adservices.java.topics.TopicsManagerFutures? from(android.content.Context context);
+    method @RequiresPermission(android.adservices.common.AdServicesPermissions.ACCESS_ADSERVICES_TOPICS) public abstract com.google.common.util.concurrent.ListenableFuture<androidx.privacysandbox.ads.adservices.topics.GetTopicsResponse> getTopicsAsync(androidx.privacysandbox.ads.adservices.topics.GetTopicsRequest request);
+    field public static final androidx.privacysandbox.ads.adservices.java.topics.TopicsManagerFutures.Companion Companion;
+  }
+
+  public static final class TopicsManagerFutures.Companion {
+    method public androidx.privacysandbox.ads.adservices.java.topics.TopicsManagerFutures? from(android.content.Context context);
+  }
+
+}
+
diff --git a/privacysandbox/ads/ads-adservices-java/api/public_plus_experimental_current.txt b/privacysandbox/ads/ads-adservices-java/api/public_plus_experimental_current.txt
new file mode 100644
index 0000000..26eea8b
--- /dev/null
+++ b/privacysandbox/ads/ads-adservices-java/api/public_plus_experimental_current.txt
@@ -0,0 +1,92 @@
+// Signature format: 4.0
+package androidx.privacysandbox.ads.adservices.java.adid {
+
+  public abstract class AdIdManagerFutures {
+    method public static final androidx.privacysandbox.ads.adservices.java.adid.AdIdManagerFutures? from(android.content.Context context);
+    method @RequiresPermission(android.adservices.common.AdServicesPermissions.ACCESS_ADSERVICES_AD_ID) public abstract com.google.common.util.concurrent.ListenableFuture<androidx.privacysandbox.ads.adservices.adid.AdId> getAdIdAsync();
+    field public static final androidx.privacysandbox.ads.adservices.java.adid.AdIdManagerFutures.Companion Companion;
+  }
+
+  public static final class AdIdManagerFutures.Companion {
+    method public androidx.privacysandbox.ads.adservices.java.adid.AdIdManagerFutures? from(android.content.Context context);
+  }
+
+}
+
+package androidx.privacysandbox.ads.adservices.java.adselection {
+
+  public abstract class AdSelectionManagerFutures {
+    method public static final androidx.privacysandbox.ads.adservices.java.adselection.AdSelectionManagerFutures? from(android.content.Context context);
+    method @RequiresPermission(android.adservices.common.AdServicesPermissions.ACCESS_ADSERVICES_CUSTOM_AUDIENCE) public abstract com.google.common.util.concurrent.ListenableFuture<kotlin.Unit> reportImpressionAsync(androidx.privacysandbox.ads.adservices.adselection.ReportImpressionRequest reportImpressionRequest);
+    method @RequiresPermission(android.adservices.common.AdServicesPermissions.ACCESS_ADSERVICES_CUSTOM_AUDIENCE) public abstract com.google.common.util.concurrent.ListenableFuture<androidx.privacysandbox.ads.adservices.adselection.AdSelectionOutcome> selectAdsAsync(androidx.privacysandbox.ads.adservices.adselection.AdSelectionConfig adSelectionConfig);
+    field public static final androidx.privacysandbox.ads.adservices.java.adselection.AdSelectionManagerFutures.Companion Companion;
+  }
+
+  public static final class AdSelectionManagerFutures.Companion {
+    method public androidx.privacysandbox.ads.adservices.java.adselection.AdSelectionManagerFutures? from(android.content.Context context);
+  }
+
+}
+
+package androidx.privacysandbox.ads.adservices.java.appsetid {
+
+  public abstract class AppSetIdManagerFutures {
+    method public static final androidx.privacysandbox.ads.adservices.java.appsetid.AppSetIdManagerFutures? from(android.content.Context context);
+    method public abstract com.google.common.util.concurrent.ListenableFuture<androidx.privacysandbox.ads.adservices.appsetid.AppSetId> getAppSetIdAsync();
+    field public static final androidx.privacysandbox.ads.adservices.java.appsetid.AppSetIdManagerFutures.Companion Companion;
+  }
+
+  public static final class AppSetIdManagerFutures.Companion {
+    method public androidx.privacysandbox.ads.adservices.java.appsetid.AppSetIdManagerFutures? from(android.content.Context context);
+  }
+
+}
+
+package androidx.privacysandbox.ads.adservices.java.customaudience {
+
+  public abstract class CustomAudienceManagerFutures {
+    method public static final androidx.privacysandbox.ads.adservices.java.customaudience.CustomAudienceManagerFutures? from(android.content.Context context);
+    method @RequiresPermission(android.adservices.common.AdServicesPermissions.ACCESS_ADSERVICES_CUSTOM_AUDIENCE) public abstract com.google.common.util.concurrent.ListenableFuture<kotlin.Unit> joinCustomAudienceAsync(androidx.privacysandbox.ads.adservices.customaudience.JoinCustomAudienceRequest request);
+    method @RequiresPermission(android.adservices.common.AdServicesPermissions.ACCESS_ADSERVICES_CUSTOM_AUDIENCE) public abstract com.google.common.util.concurrent.ListenableFuture<kotlin.Unit> leaveCustomAudienceAsync(androidx.privacysandbox.ads.adservices.customaudience.LeaveCustomAudienceRequest request);
+    field public static final androidx.privacysandbox.ads.adservices.java.customaudience.CustomAudienceManagerFutures.Companion Companion;
+  }
+
+  public static final class CustomAudienceManagerFutures.Companion {
+    method public androidx.privacysandbox.ads.adservices.java.customaudience.CustomAudienceManagerFutures? from(android.content.Context context);
+  }
+
+}
+
+package androidx.privacysandbox.ads.adservices.java.measurement {
+
+  public abstract class MeasurementManagerFutures {
+    method public abstract com.google.common.util.concurrent.ListenableFuture<kotlin.Unit> deleteRegistrationsAsync(androidx.privacysandbox.ads.adservices.measurement.DeletionRequest deletionRequest);
+    method public static final androidx.privacysandbox.ads.adservices.java.measurement.MeasurementManagerFutures? from(android.content.Context context);
+    method @RequiresPermission(android.adservices.common.AdServicesPermissions.ACCESS_ADSERVICES_ATTRIBUTION) public abstract com.google.common.util.concurrent.ListenableFuture<java.lang.Integer> getMeasurementApiStatusAsync();
+    method @RequiresPermission(android.adservices.common.AdServicesPermissions.ACCESS_ADSERVICES_ATTRIBUTION) public abstract com.google.common.util.concurrent.ListenableFuture<kotlin.Unit> registerSourceAsync(android.net.Uri attributionSource, android.view.InputEvent? inputEvent);
+    method @RequiresPermission(android.adservices.common.AdServicesPermissions.ACCESS_ADSERVICES_ATTRIBUTION) public abstract com.google.common.util.concurrent.ListenableFuture<kotlin.Unit> registerTriggerAsync(android.net.Uri trigger);
+    method @RequiresPermission(android.adservices.common.AdServicesPermissions.ACCESS_ADSERVICES_ATTRIBUTION) public abstract com.google.common.util.concurrent.ListenableFuture<kotlin.Unit> registerWebSourceAsync(androidx.privacysandbox.ads.adservices.measurement.WebSourceRegistrationRequest request);
+    method @RequiresPermission(android.adservices.common.AdServicesPermissions.ACCESS_ADSERVICES_ATTRIBUTION) public abstract com.google.common.util.concurrent.ListenableFuture<kotlin.Unit> registerWebTriggerAsync(androidx.privacysandbox.ads.adservices.measurement.WebTriggerRegistrationRequest request);
+    field public static final androidx.privacysandbox.ads.adservices.java.measurement.MeasurementManagerFutures.Companion Companion;
+  }
+
+  public static final class MeasurementManagerFutures.Companion {
+    method public androidx.privacysandbox.ads.adservices.java.measurement.MeasurementManagerFutures? from(android.content.Context context);
+  }
+
+}
+
+package androidx.privacysandbox.ads.adservices.java.topics {
+
+  public abstract class TopicsManagerFutures {
+    method public static final androidx.privacysandbox.ads.adservices.java.topics.TopicsManagerFutures? from(android.content.Context context);
+    method @RequiresPermission(android.adservices.common.AdServicesPermissions.ACCESS_ADSERVICES_TOPICS) public abstract com.google.common.util.concurrent.ListenableFuture<androidx.privacysandbox.ads.adservices.topics.GetTopicsResponse> getTopicsAsync(androidx.privacysandbox.ads.adservices.topics.GetTopicsRequest request);
+    field public static final androidx.privacysandbox.ads.adservices.java.topics.TopicsManagerFutures.Companion Companion;
+  }
+
+  public static final class TopicsManagerFutures.Companion {
+    method public androidx.privacysandbox.ads.adservices.java.topics.TopicsManagerFutures? from(android.content.Context context);
+  }
+
+}
+
diff --git a/privacysandbox/ads/ads-adservices-java/api/res-current.txt b/privacysandbox/ads/ads-adservices-java/api/res-current.txt
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/privacysandbox/ads/ads-adservices-java/api/res-current.txt
diff --git a/privacysandbox/ads/ads-adservices-java/api/restricted_current.txt b/privacysandbox/ads/ads-adservices-java/api/restricted_current.txt
new file mode 100644
index 0000000..26eea8b
--- /dev/null
+++ b/privacysandbox/ads/ads-adservices-java/api/restricted_current.txt
@@ -0,0 +1,92 @@
+// Signature format: 4.0
+package androidx.privacysandbox.ads.adservices.java.adid {
+
+  public abstract class AdIdManagerFutures {
+    method public static final androidx.privacysandbox.ads.adservices.java.adid.AdIdManagerFutures? from(android.content.Context context);
+    method @RequiresPermission(android.adservices.common.AdServicesPermissions.ACCESS_ADSERVICES_AD_ID) public abstract com.google.common.util.concurrent.ListenableFuture<androidx.privacysandbox.ads.adservices.adid.AdId> getAdIdAsync();
+    field public static final androidx.privacysandbox.ads.adservices.java.adid.AdIdManagerFutures.Companion Companion;
+  }
+
+  public static final class AdIdManagerFutures.Companion {
+    method public androidx.privacysandbox.ads.adservices.java.adid.AdIdManagerFutures? from(android.content.Context context);
+  }
+
+}
+
+package androidx.privacysandbox.ads.adservices.java.adselection {
+
+  public abstract class AdSelectionManagerFutures {
+    method public static final androidx.privacysandbox.ads.adservices.java.adselection.AdSelectionManagerFutures? from(android.content.Context context);
+    method @RequiresPermission(android.adservices.common.AdServicesPermissions.ACCESS_ADSERVICES_CUSTOM_AUDIENCE) public abstract com.google.common.util.concurrent.ListenableFuture<kotlin.Unit> reportImpressionAsync(androidx.privacysandbox.ads.adservices.adselection.ReportImpressionRequest reportImpressionRequest);
+    method @RequiresPermission(android.adservices.common.AdServicesPermissions.ACCESS_ADSERVICES_CUSTOM_AUDIENCE) public abstract com.google.common.util.concurrent.ListenableFuture<androidx.privacysandbox.ads.adservices.adselection.AdSelectionOutcome> selectAdsAsync(androidx.privacysandbox.ads.adservices.adselection.AdSelectionConfig adSelectionConfig);
+    field public static final androidx.privacysandbox.ads.adservices.java.adselection.AdSelectionManagerFutures.Companion Companion;
+  }
+
+  public static final class AdSelectionManagerFutures.Companion {
+    method public androidx.privacysandbox.ads.adservices.java.adselection.AdSelectionManagerFutures? from(android.content.Context context);
+  }
+
+}
+
+package androidx.privacysandbox.ads.adservices.java.appsetid {
+
+  public abstract class AppSetIdManagerFutures {
+    method public static final androidx.privacysandbox.ads.adservices.java.appsetid.AppSetIdManagerFutures? from(android.content.Context context);
+    method public abstract com.google.common.util.concurrent.ListenableFuture<androidx.privacysandbox.ads.adservices.appsetid.AppSetId> getAppSetIdAsync();
+    field public static final androidx.privacysandbox.ads.adservices.java.appsetid.AppSetIdManagerFutures.Companion Companion;
+  }
+
+  public static final class AppSetIdManagerFutures.Companion {
+    method public androidx.privacysandbox.ads.adservices.java.appsetid.AppSetIdManagerFutures? from(android.content.Context context);
+  }
+
+}
+
+package androidx.privacysandbox.ads.adservices.java.customaudience {
+
+  public abstract class CustomAudienceManagerFutures {
+    method public static final androidx.privacysandbox.ads.adservices.java.customaudience.CustomAudienceManagerFutures? from(android.content.Context context);
+    method @RequiresPermission(android.adservices.common.AdServicesPermissions.ACCESS_ADSERVICES_CUSTOM_AUDIENCE) public abstract com.google.common.util.concurrent.ListenableFuture<kotlin.Unit> joinCustomAudienceAsync(androidx.privacysandbox.ads.adservices.customaudience.JoinCustomAudienceRequest request);
+    method @RequiresPermission(android.adservices.common.AdServicesPermissions.ACCESS_ADSERVICES_CUSTOM_AUDIENCE) public abstract com.google.common.util.concurrent.ListenableFuture<kotlin.Unit> leaveCustomAudienceAsync(androidx.privacysandbox.ads.adservices.customaudience.LeaveCustomAudienceRequest request);
+    field public static final androidx.privacysandbox.ads.adservices.java.customaudience.CustomAudienceManagerFutures.Companion Companion;
+  }
+
+  public static final class CustomAudienceManagerFutures.Companion {
+    method public androidx.privacysandbox.ads.adservices.java.customaudience.CustomAudienceManagerFutures? from(android.content.Context context);
+  }
+
+}
+
+package androidx.privacysandbox.ads.adservices.java.measurement {
+
+  public abstract class MeasurementManagerFutures {
+    method public abstract com.google.common.util.concurrent.ListenableFuture<kotlin.Unit> deleteRegistrationsAsync(androidx.privacysandbox.ads.adservices.measurement.DeletionRequest deletionRequest);
+    method public static final androidx.privacysandbox.ads.adservices.java.measurement.MeasurementManagerFutures? from(android.content.Context context);
+    method @RequiresPermission(android.adservices.common.AdServicesPermissions.ACCESS_ADSERVICES_ATTRIBUTION) public abstract com.google.common.util.concurrent.ListenableFuture<java.lang.Integer> getMeasurementApiStatusAsync();
+    method @RequiresPermission(android.adservices.common.AdServicesPermissions.ACCESS_ADSERVICES_ATTRIBUTION) public abstract com.google.common.util.concurrent.ListenableFuture<kotlin.Unit> registerSourceAsync(android.net.Uri attributionSource, android.view.InputEvent? inputEvent);
+    method @RequiresPermission(android.adservices.common.AdServicesPermissions.ACCESS_ADSERVICES_ATTRIBUTION) public abstract com.google.common.util.concurrent.ListenableFuture<kotlin.Unit> registerTriggerAsync(android.net.Uri trigger);
+    method @RequiresPermission(android.adservices.common.AdServicesPermissions.ACCESS_ADSERVICES_ATTRIBUTION) public abstract com.google.common.util.concurrent.ListenableFuture<kotlin.Unit> registerWebSourceAsync(androidx.privacysandbox.ads.adservices.measurement.WebSourceRegistrationRequest request);
+    method @RequiresPermission(android.adservices.common.AdServicesPermissions.ACCESS_ADSERVICES_ATTRIBUTION) public abstract com.google.common.util.concurrent.ListenableFuture<kotlin.Unit> registerWebTriggerAsync(androidx.privacysandbox.ads.adservices.measurement.WebTriggerRegistrationRequest request);
+    field public static final androidx.privacysandbox.ads.adservices.java.measurement.MeasurementManagerFutures.Companion Companion;
+  }
+
+  public static final class MeasurementManagerFutures.Companion {
+    method public androidx.privacysandbox.ads.adservices.java.measurement.MeasurementManagerFutures? from(android.content.Context context);
+  }
+
+}
+
+package androidx.privacysandbox.ads.adservices.java.topics {
+
+  public abstract class TopicsManagerFutures {
+    method public static final androidx.privacysandbox.ads.adservices.java.topics.TopicsManagerFutures? from(android.content.Context context);
+    method @RequiresPermission(android.adservices.common.AdServicesPermissions.ACCESS_ADSERVICES_TOPICS) public abstract com.google.common.util.concurrent.ListenableFuture<androidx.privacysandbox.ads.adservices.topics.GetTopicsResponse> getTopicsAsync(androidx.privacysandbox.ads.adservices.topics.GetTopicsRequest request);
+    field public static final androidx.privacysandbox.ads.adservices.java.topics.TopicsManagerFutures.Companion Companion;
+  }
+
+  public static final class TopicsManagerFutures.Companion {
+    method public androidx.privacysandbox.ads.adservices.java.topics.TopicsManagerFutures? from(android.content.Context context);
+  }
+
+}
+
diff --git a/privacysandbox/ads/ads-adservices-java/build.gradle b/privacysandbox/ads/ads-adservices-java/build.gradle
new file mode 100644
index 0000000..af73f43
--- /dev/null
+++ b/privacysandbox/ads/ads-adservices-java/build.gradle
@@ -0,0 +1,62 @@
+/*
+ * Copyright (C) 2022 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+import androidx.build.LibraryType
+
+plugins {
+    id("AndroidXPlugin")
+    id("com.android.library")
+    id("org.jetbrains.kotlin.android")
+}
+
+dependencies {
+    api(libs.kotlinStdlib)
+    api(libs.kotlinCoroutinesCore)
+    implementation("androidx.core:core-ktx:1.8.0")
+    api("androidx.annotation:annotation:1.2.0")
+
+    // To use CallbackToFutureAdapter
+    implementation "androidx.concurrent:concurrent-futures:1.1.0"
+    implementation(libs.guavaAndroid)
+    api(libs.guavaListenableFuture)
+    implementation project(path: ':privacysandbox:ads:ads-adservices')
+
+    androidTestImplementation 'org.jetbrains.kotlinx:kotlinx-coroutines-android:1.1.0'
+    androidTestImplementation project(path: ':privacysandbox:ads:ads-adservices')
+    androidTestImplementation(libs.junit)
+    androidTestImplementation(libs.kotlinTestJunit)
+    androidTestImplementation(libs.testCore)
+    androidTestImplementation(libs.testExtJunit)
+    androidTestImplementation(libs.testRunner)
+    androidTestImplementation(libs.testRules)
+    androidTestImplementation(libs.truth)
+
+    androidTestImplementation(libs.mockitoCore4, excludes.bytebuddy) // DexMaker has it"s own MockMaker
+    androidTestImplementation(libs.dexmakerMockitoInline, excludes.bytebuddy) // DexMaker has it"s own MockMaker
+}
+
+android {
+    compileSdk = 33
+    compileSdkExtension = 4
+    namespace "androidx.privacysandbox.ads.adservices.java"
+}
+
+androidx {
+    name = "androidx.privacysandbox.ads:ads-adservices-java"
+    type = LibraryType.PUBLISHED_LIBRARY
+    inceptionYear = "2022"
+    description = "write Java code to call PP APIs."
+}
diff --git a/privacysandbox/ads/ads-adservices-java/src/androidTest/AndroidManifest.xml b/privacysandbox/ads/ads-adservices-java/src/androidTest/AndroidManifest.xml
new file mode 100644
index 0000000..faff43e
--- /dev/null
+++ b/privacysandbox/ads/ads-adservices-java/src/androidTest/AndroidManifest.xml
@@ -0,0 +1,25 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+  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.
+  -->
+<manifest xmlns:android="http://schemas.android.com/apk/res/android">
+    <uses-permission android:name="android.permission.ACCESS_ADSERVICES_TOPICS" />
+    <uses-permission android:name="android.permission.ACCESS_ADSERVICES_AD_ID" />
+    <uses-permission android:name="android.permission.ACCESS_ADSERVICES_ATTRIBUTION" />
+    <application>
+        <property android:name="android.adservices.AD_SERVICES_CONFIG"
+            android:resource="@xml/ad_services_config" />
+    </application>
+</manifest>
\ No newline at end of file
diff --git a/privacysandbox/ads/ads-adservices-java/src/androidTest/java/androidx/privacysandbox/ads/adservices/java/adid/AdIdManagerFuturesTest.kt b/privacysandbox/ads/ads-adservices-java/src/androidTest/java/androidx/privacysandbox/ads/adservices/java/adid/AdIdManagerFuturesTest.kt
new file mode 100644
index 0000000..bf0a81b
--- /dev/null
+++ b/privacysandbox/ads/ads-adservices-java/src/androidTest/java/androidx/privacysandbox/ads/adservices/java/adid/AdIdManagerFuturesTest.kt
@@ -0,0 +1,111 @@
+/*
+ * 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.privacysandbox.ads.adservices.java.adid
+
+import android.content.Context
+import android.os.OutcomeReceiver
+import android.os.ext.SdkExtensions
+import androidx.annotation.RequiresExtension
+import androidx.privacysandbox.ads.adservices.adid.AdId
+import androidx.test.core.app.ApplicationProvider
+import androidx.test.ext.junit.runners.AndroidJUnit4
+import androidx.test.filters.SdkSuppress
+import androidx.test.filters.SmallTest
+import com.google.common.truth.Truth
+import com.google.common.util.concurrent.ListenableFuture
+import org.junit.Assert
+import org.junit.Assume
+import org.junit.Before
+import org.junit.Test
+import org.junit.runner.RunWith
+import org.mockito.ArgumentMatchers
+import org.mockito.Mockito
+import org.mockito.invocation.InvocationOnMock
+
+@SmallTest
+@SuppressWarnings("NewApi")
+@RunWith(AndroidJUnit4::class)
+@SdkSuppress(minSdkVersion = 30)
+class AdIdManagerFuturesTest {
+
+    @Before
+    fun setUp() {
+        mContext = Mockito.spy(ApplicationProvider.getApplicationContext<Context>())
+    }
+
+    @Test
+    @SdkSuppress(maxSdkVersion = 33, minSdkVersion = 30)
+    fun testAdIdOlderVersions() {
+        val sdkExtVersion = SdkExtensions.getExtensionVersion(SdkExtensions.AD_SERVICES)
+        Assume.assumeTrue("maxSdkVersion = API 33 ext 3", sdkExtVersion < 4)
+        Truth.assertThat(AdIdManagerFutures.from(mContext)).isEqualTo(null)
+    }
+
+    @Test
+    @RequiresExtension(extension = SdkExtensions.AD_SERVICES, version = 4)
+    fun testAdIdAsync() {
+        val sdkExtVersion = SdkExtensions.getExtensionVersion(SdkExtensions.AD_SERVICES)
+        Assume.assumeTrue("minSdkVersion = API 33 ext 4", sdkExtVersion >= 4)
+        val adIdManager = mockAdIdManager(mContext)
+        setupResponse(adIdManager)
+        val managerCompat = AdIdManagerFutures.from(mContext)
+
+        // Actually invoke the compat code.
+        val result: ListenableFuture<AdId> = managerCompat!!.getAdIdAsync()
+
+        // Verify that the result of the compat call is correct.
+        verifyResponse(result.get())
+
+        // Verify that the compat code was invoked correctly.
+        Mockito.verify(adIdManager).getAdId(ArgumentMatchers.any(), ArgumentMatchers.any())
+    }
+
+    @SuppressWarnings("NewApi")
+    @SdkSuppress(minSdkVersion = 30)
+    @RequiresExtension(extension = SdkExtensions.AD_SERVICES, version = 4)
+    companion object {
+        private lateinit var mContext: Context
+
+        private fun mockAdIdManager(spyContext: Context): android.adservices.adid.AdIdManager {
+            val adIdManager = Mockito.mock(android.adservices.adid.AdIdManager::class.java)
+            Mockito.`when`(spyContext.getSystemService(
+                android.adservices.adid.AdIdManager::class.java)).thenReturn(adIdManager)
+            return adIdManager
+        }
+
+        private fun setupResponse(adIdManager: android.adservices.adid.AdIdManager) {
+            // Set up the response that AdIdManager will return when the compat code calls it.
+            val adId = android.adservices.adid.AdId("1234", false)
+            val answer = { args: InvocationOnMock ->
+                val receiver = args.getArgument<
+                    OutcomeReceiver<android.adservices.adid.AdId, Exception>>(1)
+                receiver.onResult(adId)
+                null
+            }
+            Mockito.doAnswer(answer)
+                .`when`(adIdManager).getAdId(
+                    ArgumentMatchers.any(),
+                    ArgumentMatchers.any()
+                )
+        }
+
+        private fun verifyResponse(adId: androidx.privacysandbox.ads.adservices.adid.AdId) {
+            Assert.assertEquals("1234", adId.adId)
+            Assert.assertEquals(false, adId.isLimitAdTrackingEnabled)
+        }
+    }
+}
\ No newline at end of file
diff --git a/privacysandbox/ads/ads-adservices-java/src/androidTest/java/androidx/privacysandbox/ads/adservices/java/adselection/AdSelectionManagerFuturesTest.kt b/privacysandbox/ads/ads-adservices-java/src/androidTest/java/androidx/privacysandbox/ads/adservices/java/adselection/AdSelectionManagerFuturesTest.kt
new file mode 100644
index 0000000..ff87882
--- /dev/null
+++ b/privacysandbox/ads/ads-adservices-java/src/androidTest/java/androidx/privacysandbox/ads/adservices/java/adselection/AdSelectionManagerFuturesTest.kt
@@ -0,0 +1,236 @@
+/*
+ * 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.privacysandbox.ads.adservices.java.adselection
+
+import android.content.Context
+import android.net.Uri
+import android.os.OutcomeReceiver
+import android.os.ext.SdkExtensions
+import androidx.annotation.RequiresExtension
+import androidx.privacysandbox.ads.adservices.adselection.AdSelectionConfig
+import androidx.privacysandbox.ads.adservices.adselection.AdSelectionOutcome
+import androidx.privacysandbox.ads.adservices.adselection.ReportImpressionRequest
+import androidx.test.core.app.ApplicationProvider
+import androidx.privacysandbox.ads.adservices.common.AdSelectionSignals
+import androidx.privacysandbox.ads.adservices.common.AdTechIdentifier
+import androidx.privacysandbox.ads.adservices.java.adselection.AdSelectionManagerFutures.Companion.from
+import androidx.test.ext.junit.runners.AndroidJUnit4
+import androidx.test.filters.SdkSuppress
+import androidx.test.filters.SmallTest
+import com.google.common.truth.Truth
+import com.google.common.util.concurrent.ListenableFuture
+import org.junit.Assert
+import org.junit.Assume
+import org.junit.Before
+import org.junit.Test
+import org.junit.runner.RunWith
+import org.mockito.ArgumentCaptor
+import org.mockito.ArgumentMatchers.any
+import org.mockito.Mockito.doAnswer
+import org.mockito.Mockito.mock
+import org.mockito.Mockito.spy
+import org.mockito.Mockito.verify
+import org.mockito.Mockito.`when`
+import org.mockito.invocation.InvocationOnMock
+
+@SmallTest
+@SuppressWarnings("NewApi")
+@RunWith(AndroidJUnit4::class)
+@SdkSuppress(minSdkVersion = 30)
+class AdSelectionManagerFuturesTest {
+
+    @Before
+    fun setUp() {
+        mContext = spy(ApplicationProvider.getApplicationContext<Context>())
+    }
+
+    @Test
+    @SdkSuppress(maxSdkVersion = 33, minSdkVersion = 30)
+    fun testAdSelectionOlderVersions() {
+        val sdkExtVersion = SdkExtensions.getExtensionVersion(SdkExtensions.AD_SERVICES)
+
+        Assume.assumeTrue("maxSdkVersion = API 33 ext 3", sdkExtVersion < 4)
+        Truth.assertThat(from(mContext)).isEqualTo(null)
+    }
+
+    @Test
+    @RequiresExtension(extension = SdkExtensions.AD_SERVICES, version = 4)
+    fun testSelectAds() {
+        val sdkExtVersion = SdkExtensions.getExtensionVersion(SdkExtensions.AD_SERVICES)
+
+        Assume.assumeTrue("minSdkVersion = API 33 ext 4", sdkExtVersion >= 4)
+        val adSelectionManager = mockAdSelectionManager(mContext)
+        setupAdSelectionResponse(adSelectionManager)
+        val managerCompat = from(mContext)
+
+        // Actually invoke the compat code.
+        val result: ListenableFuture<AdSelectionOutcome> =
+            managerCompat!!.selectAdsAsync(adSelectionConfig)
+
+        // Verify that the result of the compat call is correct.
+        verifyResponse(result.get())
+
+        // Verify that the compat code was invoked correctly.
+        val captor = ArgumentCaptor.forClass(
+            android.adservices.adselection.AdSelectionConfig::class.java)
+        verify(adSelectionManager).selectAds(captor.capture(), any(), any())
+
+        // Verify that the request that the compat code makes to the platform is correct.
+        verifyRequest(captor.value)
+    }
+
+    @Test
+    @SuppressWarnings("NewApi")
+    @RequiresExtension(extension = SdkExtensions.AD_SERVICES, version = 4)
+    fun testReportImpression() {
+        val sdkExtVersion = SdkExtensions.getExtensionVersion(SdkExtensions.AD_SERVICES)
+
+        Assume.assumeTrue("minSdkVersion = API 33 ext 4", sdkExtVersion >= 4)
+        val adSelectionManager = mockAdSelectionManager(mContext)
+        setupAdSelectionResponse(adSelectionManager)
+        val managerCompat = from(mContext)
+        val reportImpressionRequest = ReportImpressionRequest(adSelectionId, adSelectionConfig)
+
+        // Actually invoke the compat code.
+        managerCompat!!.reportImpressionAsync(reportImpressionRequest).get()
+
+        // Verify that the compat code was invoked correctly.
+        val captor = ArgumentCaptor.forClass(
+            android.adservices.adselection.ReportImpressionRequest::class.java)
+        verify(adSelectionManager).reportImpression(captor.capture(), any(), any())
+
+        // Verify that the request that the compat code makes to the platform is correct.
+        verifyReportImpressionRequest(captor.value)
+    }
+
+    @SuppressWarnings("NewApi")
+    @SdkSuppress(minSdkVersion = 30)
+    @RequiresExtension(extension = SdkExtensions.AD_SERVICES, version = 4)
+    companion object {
+        private lateinit var mContext: Context
+        private const val adSelectionId = 1234L
+        private const val adId = "1234"
+        private val seller: AdTechIdentifier = AdTechIdentifier(adId)
+        private val decisionLogicUri: Uri = Uri.parse("www.abc.com")
+        private val customAudienceBuyers: List<AdTechIdentifier> = listOf(seller)
+        private const val adSelectionSignalsStr = "adSelSignals"
+        private val adSelectionSignals: AdSelectionSignals =
+            AdSelectionSignals(adSelectionSignalsStr)
+        private const val sellerSignalsStr = "sellerSignals"
+        private val sellerSignals: AdSelectionSignals = AdSelectionSignals(sellerSignalsStr)
+        private val perBuyerSignals: Map<AdTechIdentifier, AdSelectionSignals> =
+            mutableMapOf(Pair(seller, sellerSignals))
+        private val trustedScoringSignalsUri: Uri = Uri.parse("www.xyz.com")
+        private val adSelectionConfig = AdSelectionConfig(
+            seller,
+            decisionLogicUri,
+            customAudienceBuyers,
+            adSelectionSignals,
+            sellerSignals,
+            perBuyerSignals,
+            trustedScoringSignalsUri)
+
+        // Response.
+        private val renderUri = Uri.parse("render-uri.com")
+
+        private fun mockAdSelectionManager(
+            spyContext: Context
+        ): android.adservices.adselection.AdSelectionManager {
+            val adSelectionManager =
+                mock(android.adservices.adselection.AdSelectionManager::class.java)
+            `when`(spyContext.getSystemService(
+                android.adservices.adselection.AdSelectionManager::class.java))
+                .thenReturn(adSelectionManager)
+            return adSelectionManager
+        }
+
+        private fun setupAdSelectionResponse(
+            adSelectionManager: android.adservices.adselection.AdSelectionManager
+        ) {
+            // Set up the response that AdSelectionManager will return when the compat code calls
+            // it.
+            val response = android.adservices.adselection.AdSelectionOutcome.Builder()
+                .setAdSelectionId(adSelectionId)
+                .setRenderUri(renderUri)
+                .build()
+            val answer = { args: InvocationOnMock ->
+                val receiver = args.getArgument<OutcomeReceiver<
+                    android.adservices.adselection.AdSelectionOutcome, Exception>>(2)
+                receiver.onResult(response)
+                null
+            }
+            doAnswer(answer)
+                .`when`(adSelectionManager).selectAds(
+                    any(),
+                    any(),
+                    any()
+                )
+
+            val answer2 = { args: InvocationOnMock ->
+                val receiver = args.getArgument<OutcomeReceiver<Any, Exception>>(2)
+                receiver.onResult(Object())
+                null
+            }
+            doAnswer(answer2).`when`(adSelectionManager).reportImpression(any(), any(), any())
+        }
+
+        private fun verifyRequest(request: android.adservices.adselection.AdSelectionConfig) {
+            // Set up the request that we expect the compat code to invoke.
+            val expectedRequest = getPlatformAdSelectionConfig()
+
+            Assert.assertEquals(expectedRequest, request)
+        }
+
+        private fun verifyResponse(
+            outcome: androidx.privacysandbox.ads.adservices.adselection.AdSelectionOutcome
+        ) {
+            val expectedOutcome =
+                androidx.privacysandbox.ads.adservices.adselection.AdSelectionOutcome(
+                    adSelectionId,
+                    renderUri)
+            Assert.assertEquals(expectedOutcome, outcome)
+        }
+
+        private fun getPlatformAdSelectionConfig():
+            android.adservices.adselection.AdSelectionConfig {
+            val adTechIdentifier = android.adservices.common.AdTechIdentifier.fromString(adId)
+            return android.adservices.adselection.AdSelectionConfig.Builder()
+                .setAdSelectionSignals(
+                    android.adservices.common.AdSelectionSignals.fromString(adSelectionSignalsStr))
+                .setCustomAudienceBuyers(listOf(adTechIdentifier))
+                .setDecisionLogicUri(decisionLogicUri)
+                .setPerBuyerSignals(mutableMapOf(Pair(
+                    adTechIdentifier,
+                    android.adservices.common.AdSelectionSignals.fromString(sellerSignalsStr))))
+                .setSeller(adTechIdentifier)
+                .setSellerSignals(
+                    android.adservices.common.AdSelectionSignals.fromString(sellerSignalsStr))
+                .setTrustedScoringSignalsUri(trustedScoringSignalsUri)
+                .build()
+        }
+
+        private fun verifyReportImpressionRequest(
+            request: android.adservices.adselection.ReportImpressionRequest
+        ) {
+            val expectedRequest = android.adservices.adselection.ReportImpressionRequest(
+                adSelectionId,
+                getPlatformAdSelectionConfig())
+            Assert.assertEquals(expectedRequest.adSelectionId, request.adSelectionId)
+            Assert.assertEquals(expectedRequest.adSelectionConfig, request.adSelectionConfig)
+        }
+    }
+}
\ No newline at end of file
diff --git a/privacysandbox/ads/ads-adservices-java/src/androidTest/java/androidx/privacysandbox/ads/adservices/java/appsetid/AppSetIdManagerFuturesTest.kt b/privacysandbox/ads/ads-adservices-java/src/androidTest/java/androidx/privacysandbox/ads/adservices/java/appsetid/AppSetIdManagerFuturesTest.kt
new file mode 100644
index 0000000..a558f76
--- /dev/null
+++ b/privacysandbox/ads/ads-adservices-java/src/androidTest/java/androidx/privacysandbox/ads/adservices/java/appsetid/AppSetIdManagerFuturesTest.kt
@@ -0,0 +1,122 @@
+/*
+ * 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.privacysandbox.ads.adservices.java.appsetid
+
+import android.content.Context
+import android.os.OutcomeReceiver
+import android.os.ext.SdkExtensions
+import androidx.annotation.RequiresExtension
+import androidx.privacysandbox.ads.adservices.appsetid.AppSetId
+import androidx.test.core.app.ApplicationProvider
+import androidx.test.ext.junit.runners.AndroidJUnit4
+import androidx.test.filters.SdkSuppress
+import androidx.test.filters.SmallTest
+import com.google.common.truth.Truth.assertThat
+import com.google.common.util.concurrent.ListenableFuture
+import org.junit.Assert
+import org.junit.Assume
+import org.junit.Before
+import org.junit.Test
+import org.junit.runner.RunWith
+import org.mockito.ArgumentMatchers.any
+import org.mockito.Mockito.doAnswer
+import org.mockito.Mockito.mock
+import org.mockito.Mockito.spy
+import org.mockito.Mockito.verify
+import org.mockito.Mockito.`when`
+import org.mockito.invocation.InvocationOnMock
+
+@SmallTest
+@RunWith(AndroidJUnit4::class)
+@SdkSuppress(minSdkVersion = 30)
+class AppSetIdManagerFuturesTest {
+
+    @Before
+    fun setUp() {
+        mContext = spy(ApplicationProvider.getApplicationContext<Context>())
+    }
+
+    @Test
+    @SdkSuppress(maxSdkVersion = 33, minSdkVersion = 30)
+    fun testAppSetIdOlderVersions() {
+        val sdkExtVersion = SdkExtensions.getExtensionVersion(SdkExtensions.AD_SERVICES)
+
+        Assume.assumeTrue("maxSdkVersion = API 33 ext 3", sdkExtVersion < 4)
+        assertThat(AppSetIdManagerFutures.from(mContext)).isEqualTo(null)
+    }
+
+    @Test
+    @SuppressWarnings("NewApi")
+    @RequiresExtension(extension = SdkExtensions.AD_SERVICES, version = 4)
+    fun testAppSetIdAsync() {
+        val sdkExtVersion = SdkExtensions.getExtensionVersion(SdkExtensions.AD_SERVICES)
+
+        Assume.assumeTrue("minSdkVersion = API 33 ext 4", sdkExtVersion >= 4)
+        val appSetIdManager = mockAppSetIdManager(mContext)
+        setupResponse(appSetIdManager)
+        val managerCompat = AppSetIdManagerFutures.from(mContext)
+
+        // Actually invoke the compat code.
+        val result: ListenableFuture<AppSetId> = managerCompat!!.getAppSetIdAsync()
+
+        // Verify that the result of the compat call is correct.
+        verifyResponse(result.get())
+
+        // Verify that the compat code was invoked correctly.
+        verify(appSetIdManager).getAppSetId(any(), any())
+    }
+
+    @SuppressWarnings("NewApi")
+    @SdkSuppress(minSdkVersion = 30)
+    @RequiresExtension(extension = SdkExtensions.AD_SERVICES, version = 4)
+    companion object {
+        private lateinit var mContext: Context
+
+        private fun mockAppSetIdManager(
+            spyContext: Context
+        ): android.adservices.appsetid.AppSetIdManager {
+            val appSetIdManager = mock(android.adservices.appsetid.AppSetIdManager::class.java)
+            `when`(spyContext.getSystemService(
+                android.adservices.appsetid.AppSetIdManager::class.java))
+                .thenReturn(appSetIdManager)
+            return appSetIdManager
+        }
+
+        private fun setupResponse(appSetIdManager: android.adservices.appsetid.AppSetIdManager) {
+            // Set up the response that AdIdManager will return when the compat code calls it.
+            val appSetId = android.adservices.appsetid.AppSetId(
+                "1234",
+                android.adservices.appsetid.AppSetId.SCOPE_APP)
+            val answer = { args: InvocationOnMock ->
+                val receiver = args.getArgument<
+                    OutcomeReceiver<android.adservices.appsetid.AppSetId, Exception>>(1)
+                receiver.onResult(appSetId)
+                null
+            }
+            doAnswer(answer)
+                .`when`(appSetIdManager).getAppSetId(
+                    any(),
+                    any()
+                )
+        }
+
+        private fun verifyResponse(appSetId: AppSetId) {
+            Assert.assertEquals("1234", appSetId.id)
+            Assert.assertEquals(AppSetId.SCOPE_APP, appSetId.scope)
+        }
+    }
+}
\ No newline at end of file
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
new file mode 100644
index 0000000..1ebf7e7
--- /dev/null
+++ b/privacysandbox/ads/ads-adservices-java/src/androidTest/java/androidx/privacysandbox/ads/adservices/java/customaudience/CustomAudienceManagerFuturesTest.kt
@@ -0,0 +1,232 @@
+/*
+ * 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.privacysandbox.ads.adservices.java.customaudience
+
+import android.adservices.customaudience.CustomAudienceManager
+import android.content.Context
+import android.net.Uri
+import android.os.OutcomeReceiver
+import android.os.ext.SdkExtensions
+import androidx.annotation.RequiresExtension
+import androidx.privacysandbox.ads.adservices.common.AdData
+import androidx.privacysandbox.ads.adservices.common.AdSelectionSignals
+import androidx.privacysandbox.ads.adservices.common.AdTechIdentifier
+import androidx.privacysandbox.ads.adservices.customaudience.CustomAudience
+import androidx.privacysandbox.ads.adservices.java.customaudience.CustomAudienceManagerFutures.Companion.from
+import androidx.privacysandbox.ads.adservices.customaudience.JoinCustomAudienceRequest
+import androidx.privacysandbox.ads.adservices.customaudience.LeaveCustomAudienceRequest
+import androidx.privacysandbox.ads.adservices.customaudience.TrustedBiddingData
+import androidx.test.core.app.ApplicationProvider
+import androidx.test.ext.junit.runners.AndroidJUnit4
+import androidx.test.filters.SdkSuppress
+import androidx.test.filters.SmallTest
+import com.google.common.truth.Truth
+import java.time.Instant
+import org.junit.Assume
+import org.junit.Before
+import org.junit.Test
+import org.junit.runner.RunWith
+import org.mockito.ArgumentCaptor
+import org.mockito.ArgumentMatchers.any
+import org.mockito.Mockito.doAnswer
+import org.mockito.Mockito.mock
+import org.mockito.Mockito.spy
+import org.mockito.Mockito.verify
+import org.mockito.Mockito.`when`
+import org.mockito.invocation.InvocationOnMock
+
+@SmallTest
+@SuppressWarnings("NewApi")
+@RunWith(AndroidJUnit4::class)
+@SdkSuppress(minSdkVersion = 30)
+class CustomAudienceManagerFuturesTest {
+
+    @Before
+    fun setUp() {
+        mContext = spy(ApplicationProvider.getApplicationContext<Context>())
+    }
+
+    @Test
+    @SdkSuppress(maxSdkVersion = 33, minSdkVersion = 30)
+    fun testOlderVersions() {
+        val sdkExtVersion = SdkExtensions.getExtensionVersion(SdkExtensions.AD_SERVICES)
+
+        Assume.assumeTrue("maxSdkVersion = API 33 ext 3", sdkExtVersion < 4)
+        Truth.assertThat(from(mContext)).isEqualTo(null)
+    }
+
+    @Test
+    @RequiresExtension(extension = SdkExtensions.AD_SERVICES, version = 4)
+    fun testJoinCustomAudience() {
+        val sdkExtVersion = SdkExtensions.getExtensionVersion(SdkExtensions.AD_SERVICES)
+
+        Assume.assumeTrue("minSdkVersion = API 33 ext 4", sdkExtVersion >= 4)
+        val customAudienceManager = mockCustomAudienceManager(mContext)
+        setupResponse(customAudienceManager)
+        val managerCompat = from(mContext)
+
+        // Actually invoke the compat code.
+        val customAudience = CustomAudience.Builder(buyer, name, uri, uri, ads)
+            .setActivationTime(Instant.now())
+            .setExpirationTime(Instant.now())
+            .setUserBiddingSignals(userBiddingSignals)
+            .setTrustedBiddingData(trustedBiddingSignals)
+            .build()
+        val request = JoinCustomAudienceRequest(customAudience)
+        managerCompat!!.joinCustomAudienceAsync(request).get()
+
+        // Verify that the compat code was invoked correctly.
+        val captor = ArgumentCaptor.forClass(
+            android.adservices.customaudience.JoinCustomAudienceRequest::class.java
+        )
+        verify(customAudienceManager).joinCustomAudience(captor.capture(), any(), any())
+
+        // Verify that the request that the compat code makes to the platform is correct.
+        verifyJoinCustomAudienceRequest(captor.value)
+    }
+
+    @Test
+    @RequiresExtension(extension = SdkExtensions.AD_SERVICES, version = 4)
+    fun testLeaveCustomAudience() {
+        val sdkExtVersion = SdkExtensions.getExtensionVersion(SdkExtensions.AD_SERVICES)
+
+        Assume.assumeTrue("minSdkVersion = API 33 ext 4", sdkExtVersion >= 4)
+        val customAudienceManager = mockCustomAudienceManager(mContext)
+        setupResponse(customAudienceManager)
+        val managerCompat = from(mContext)
+
+        // Actually invoke the compat code.
+        val request = LeaveCustomAudienceRequest(buyer, name)
+        managerCompat!!.leaveCustomAudienceAsync(request).get()
+
+        // Verify that the compat code was invoked correctly.
+        val captor = ArgumentCaptor.forClass(
+            android.adservices.customaudience.LeaveCustomAudienceRequest::class.java
+        )
+        verify(customAudienceManager).leaveCustomAudience(captor.capture(), any(), any())
+
+        // Verify that the request that the compat code makes to the platform is correct.
+        verifyLeaveCustomAudienceRequest(captor.value)
+    }
+
+    @SdkSuppress(minSdkVersion = 30)
+    @RequiresExtension(extension = SdkExtensions.AD_SERVICES, version = 4)
+    companion object {
+        private lateinit var mContext: Context
+        private val uri: Uri = Uri.parse("abc.com")
+        private const val adtech = "1234"
+        private val buyer: AdTechIdentifier = AdTechIdentifier(adtech)
+        private const val name: String = "abc"
+        private const val signals = "signals"
+        private val userBiddingSignals: AdSelectionSignals = AdSelectionSignals(signals)
+        private val keys: List<String> = listOf("key1", "key2")
+        private val trustedBiddingSignals: TrustedBiddingData = TrustedBiddingData(uri, keys)
+        private const val metadata = "metadata"
+        private val ads: List<AdData> = listOf(AdData(uri, metadata))
+
+        private fun mockCustomAudienceManager(spyContext: Context): CustomAudienceManager {
+            val customAudienceManager = mock(CustomAudienceManager::class.java)
+            `when`(spyContext.getSystemService(CustomAudienceManager::class.java))
+                .thenReturn(customAudienceManager)
+            return customAudienceManager
+        }
+
+        private fun setupResponse(customAudienceManager: CustomAudienceManager) {
+            val answer = { args: InvocationOnMock ->
+                val receiver = args.getArgument<OutcomeReceiver<Any, Exception>>(2)
+                receiver.onResult(Object())
+                null
+            }
+            doAnswer(answer).`when`(customAudienceManager).joinCustomAudience(any(), any(), any())
+            doAnswer(answer).`when`(customAudienceManager).leaveCustomAudience(any(), any(), any())
+        }
+
+        private fun verifyJoinCustomAudienceRequest(
+            joinCustomAudienceRequest: android.adservices.customaudience.JoinCustomAudienceRequest
+        ) {
+            // Set up the request that we expect the compat code to invoke.
+            val adtechIdentifier = android.adservices.common.AdTechIdentifier.fromString(adtech)
+            val userBiddingSignals =
+                android.adservices.common.AdSelectionSignals.fromString(signals)
+            val trustedBiddingSignals =
+                android.adservices.customaudience.TrustedBiddingData.Builder()
+                    .setTrustedBiddingKeys(keys)
+                    .setTrustedBiddingUri(uri)
+                    .build()
+            val customAudience = android.adservices.customaudience.CustomAudience.Builder()
+                .setBuyer(adtechIdentifier)
+                .setName(name)
+                .setActivationTime(Instant.now())
+                .setExpirationTime(Instant.now())
+                .setBiddingLogicUri(uri)
+                .setDailyUpdateUri(uri)
+                .setUserBiddingSignals(userBiddingSignals)
+                .setTrustedBiddingData(trustedBiddingSignals)
+                .setAds(listOf(android.adservices.common.AdData.Builder()
+                    .setRenderUri(uri)
+                    .setMetadata(metadata)
+                    .build()))
+                .build()
+
+            val expectedRequest =
+                android.adservices.customaudience.JoinCustomAudienceRequest.Builder()
+                    .setCustomAudience(customAudience)
+                    .build()
+
+            // Verify that the actual request matches the expected one.
+            Truth.assertThat(expectedRequest.customAudience.ads.size ==
+                joinCustomAudienceRequest.customAudience.ads.size).isTrue()
+            Truth.assertThat(expectedRequest.customAudience.ads[0].renderUri ==
+                joinCustomAudienceRequest.customAudience.ads[0].renderUri).isTrue()
+            Truth.assertThat(expectedRequest.customAudience.ads[0].metadata ==
+                joinCustomAudienceRequest.customAudience.ads[0].metadata).isTrue()
+            Truth.assertThat(expectedRequest.customAudience.biddingLogicUri ==
+                joinCustomAudienceRequest.customAudience.biddingLogicUri).isTrue()
+            Truth.assertThat(expectedRequest.customAudience.buyer.toString() ==
+                joinCustomAudienceRequest.customAudience.buyer.toString()).isTrue()
+            Truth.assertThat(expectedRequest.customAudience.dailyUpdateUri ==
+                joinCustomAudienceRequest.customAudience.dailyUpdateUri).isTrue()
+            Truth.assertThat(expectedRequest.customAudience.name ==
+                joinCustomAudienceRequest.customAudience.name).isTrue()
+            Truth.assertThat(trustedBiddingSignals.trustedBiddingKeys ==
+                joinCustomAudienceRequest.customAudience.trustedBiddingData!!.trustedBiddingKeys)
+                .isTrue()
+            Truth.assertThat(trustedBiddingSignals.trustedBiddingUri ==
+                joinCustomAudienceRequest.customAudience.trustedBiddingData!!.trustedBiddingUri)
+                .isTrue()
+            Truth.assertThat(
+                joinCustomAudienceRequest.customAudience.userBiddingSignals!!.toString() ==
+                signals).isTrue()
+        }
+
+        private fun verifyLeaveCustomAudienceRequest(
+            leaveCustomAudienceRequest: android.adservices.customaudience.LeaveCustomAudienceRequest
+        ) {
+            // Set up the request that we expect the compat code to invoke.
+            val adtechIdentifier = android.adservices.common.AdTechIdentifier.fromString(adtech)
+
+            val expectedRequest = android.adservices.customaudience.LeaveCustomAudienceRequest
+                .Builder()
+                .setBuyer(adtechIdentifier)
+                .setName(name)
+                .build()
+
+            // Verify that the actual request matches the expected one.
+            Truth.assertThat(expectedRequest == leaveCustomAudienceRequest).isTrue()
+        }
+    }
+}
\ No newline at end of file
diff --git a/privacysandbox/ads/ads-adservices-java/src/androidTest/java/androidx/privacysandbox/ads/adservices/java/endtoend/TestUtil.java b/privacysandbox/ads/ads-adservices-java/src/androidTest/java/androidx/privacysandbox/ads/adservices/java/endtoend/TestUtil.java
new file mode 100644
index 0000000..8d5d99a
--- /dev/null
+++ b/privacysandbox/ads/ads-adservices-java/src/androidTest/java/androidx/privacysandbox/ads/adservices/java/endtoend/TestUtil.java
@@ -0,0 +1,168 @@
+/*
+ * 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.privacysandbox.ads.adservices.java.endtoend;
+
+import android.app.Instrumentation;
+import android.content.Intent;
+import android.content.pm.PackageManager;
+import android.content.pm.ResolveInfo;
+import android.content.pm.ServiceInfo;
+import android.os.Build;
+import android.util.Log;
+
+import androidx.test.core.app.ApplicationProvider;
+
+import java.util.List;
+
+public class TestUtil {
+    private Instrumentation mInstrumentation;
+    private String mTag;
+    // Used to get the package name. Copied over from com.android.adservices.AdServicesCommon
+    private static final String TOPICS_SERVICE_NAME = "android.adservices.TOPICS_SERVICE";
+    // The JobId of the Epoch Computation.
+    private static final int EPOCH_JOB_ID = 2;
+
+    public TestUtil(Instrumentation instrumentation, String tag) {
+        mInstrumentation = instrumentation;
+        mTag = tag;
+    }
+    // Run shell command.
+    private void runShellCommand(String command) {
+        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN_MR2) {
+            if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
+                mInstrumentation.getUiAutomation().executeShellCommand(command);
+            }
+        }
+    }
+    public void overrideKillSwitches(boolean override) {
+        if (override) {
+            runShellCommand("setprop debug.adservices.global_kill_switch " + false);
+            runShellCommand("setprop debug.adservices.topics_kill_switch " + false);
+        } else {
+            runShellCommand("setprop debug.adservices.global_kill_switch " + null);
+            runShellCommand("setprop debug.adservices.topics_kill_switch " + null);
+        }
+    }
+
+    public void enableEnrollmentCheck(boolean enable) {
+        runShellCommand(
+                "setprop debug.adservices.disable_topics_enrollment_check " + enable);
+    }
+
+    // Override the Epoch Period to shorten the Epoch Length in the test.
+    public void overrideEpochPeriod(long overrideEpochPeriod) {
+        runShellCommand(
+                "setprop debug.adservices.topics_epoch_job_period_ms " + overrideEpochPeriod);
+    }
+
+    // Override the Percentage For Random Topic in the test.
+    public void overridePercentageForRandomTopic(long overridePercentage) {
+        runShellCommand(
+                "setprop debug.adservices.topics_percentage_for_random_topics "
+                        + overridePercentage);
+    }
+
+    /** Forces JobScheduler to run the Epoch Computation job */
+    public void forceEpochComputationJob() {
+        runShellCommand(
+                "cmd jobscheduler run -f" + " " + getAdServicesPackageName() + " " + EPOCH_JOB_ID);
+    }
+
+    public void overrideConsentManagerDebugMode(boolean override) {
+        String overrideStr = override ? "true" : "null";
+        runShellCommand("setprop debug.adservices.consent_manager_debug_mode " + overrideStr);
+    }
+
+    public void overrideAllowlists(boolean override) {
+        String overrideStr = override ? "*" : "null";
+        runShellCommand("device_config put adservices ppapi_app_allow_list " + overrideStr);
+        runShellCommand("device_config put adservices ppapi_app_signature_allow_list "
+                + overrideStr);
+        runShellCommand(
+                "device_config put adservices web_context_client_allow_list " + overrideStr);
+    }
+
+    public void overrideAdIdKillSwitch(boolean override) {
+        if (override) {
+            runShellCommand("setprop debug.adservices.adid_kill_switch " + false);
+        } else {
+            runShellCommand("setprop debug.adservices.adid_kill_switch " + null);
+        }
+    }
+
+    // Override measurement related kill switch to ignore the effect of actual PH values.
+    // If isOverride = true, override measurement related kill switch to OFF to allow adservices
+    // If isOverride = false, override measurement related kill switch to meaningless value so that
+    // PhFlags will use the default value.
+    public void overrideMeasurementKillSwitches(boolean isOverride) {
+        String overrideString = isOverride ? "false" : "null";
+        runShellCommand("setprop debug.adservices.global_kill_switch " + overrideString);
+        runShellCommand("setprop debug.adservices.measurement_kill_switch " + overrideString);
+        runShellCommand("setprop debug.adservices.measurement_api_register_source_kill_switch "
+                + overrideString);
+        runShellCommand("setprop debug.adservices.measurement_api_register_trigger_kill_switch "
+                + overrideString);
+        runShellCommand("setprop debug.adservices.measurement_api_register_web_source_kill_switch "
+                + overrideString);
+        runShellCommand("setprop debug.adservices.measurement_api_register_web_trigger_kill_switch "
+                + overrideString);
+        runShellCommand("setprop debug.adservices.measurement_api_delete_registrations_kill_switch "
+                + overrideString);
+        runShellCommand("setprop debug.adservices.measurement_api_status_kill_switch "
+                + overrideString);
+    }
+
+    // Override the flag to disable Measurement enrollment check. Setting to 1 disables enforcement.
+    public void overrideDisableMeasurementEnrollmentCheck(String val) {
+        runShellCommand("setprop debug.adservices.disable_measurement_enrollment_check " + val);
+    }
+
+    public void resetOverrideDisableMeasurementEnrollmentCheck() {
+        runShellCommand("setprop debug.adservices.disable_measurement_enrollment_check null");
+    }
+
+    @SuppressWarnings("deprecation")
+    // Used to get the package name. Copied over from com.android.adservices.AndroidServiceBinder
+    public String getAdServicesPackageName() {
+        final Intent intent = new Intent(TOPICS_SERVICE_NAME);
+        final List<ResolveInfo> resolveInfos = ApplicationProvider.getApplicationContext()
+                .getPackageManager()
+                .queryIntentServices(intent, PackageManager.MATCH_SYSTEM_ONLY);
+
+        if (resolveInfos == null || resolveInfos.isEmpty()) {
+            Log.e(mTag, "Failed to find resolveInfo for adServices service. Intent action: "
+                            + TOPICS_SERVICE_NAME);
+            return null;
+        }
+
+        if (resolveInfos.size() > 1) {
+            String str = String.format(
+                    "Found multiple services (%1$s) for the same intent action (%2$s)",
+                    TOPICS_SERVICE_NAME, resolveInfos);
+            Log.e(mTag, str);
+            return null;
+        }
+
+        final ServiceInfo serviceInfo = resolveInfos.get(0).serviceInfo;
+        if (serviceInfo == null) {
+            Log.e(mTag, "Failed to find serviceInfo for adServices service");
+            return null;
+        }
+
+        return serviceInfo.packageName;
+    }
+}
diff --git a/privacysandbox/ads/ads-adservices-java/src/androidTest/java/androidx/privacysandbox/ads/adservices/java/endtoend/adid/AdIdManagerTest.java b/privacysandbox/ads/ads-adservices-java/src/androidTest/java/androidx/privacysandbox/ads/adservices/java/endtoend/adid/AdIdManagerTest.java
new file mode 100644
index 0000000..20294a1
--- /dev/null
+++ b/privacysandbox/ads/ads-adservices-java/src/androidTest/java/androidx/privacysandbox/ads/adservices/java/endtoend/adid/AdIdManagerTest.java
@@ -0,0 +1,68 @@
+/*
+ * 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.privacysandbox.ads.adservices.java.endtoend.adid;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import androidx.privacysandbox.ads.adservices.adid.AdId;
+import androidx.privacysandbox.ads.adservices.internal.AdServicesInfo;
+import androidx.privacysandbox.ads.adservices.java.adid.AdIdManagerFutures;
+import androidx.privacysandbox.ads.adservices.java.endtoend.TestUtil;
+import androidx.test.core.app.ApplicationProvider;
+import androidx.test.platform.app.InstrumentationRegistry;
+
+import org.junit.After;
+import org.junit.Assume;
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.junit.runners.JUnit4;
+
+
+@RunWith(JUnit4.class)
+public class AdIdManagerTest {
+    private static final String TAG = "AdIdManagerTest";
+    private TestUtil mTestUtil = new TestUtil(InstrumentationRegistry.getInstrumentation(), TAG);
+
+    @Before
+    public void setup() throws Exception {
+        mTestUtil.overrideAdIdKillSwitch(true);
+        mTestUtil.overrideKillSwitches(true);
+        mTestUtil.overrideConsentManagerDebugMode(true);
+        mTestUtil.overrideAllowlists(true);
+    }
+
+    @After
+    public void teardown() {
+        mTestUtil.overrideAdIdKillSwitch(false);
+        mTestUtil.overrideKillSwitches(false);
+        mTestUtil.overrideConsentManagerDebugMode(false);
+        mTestUtil.overrideAllowlists(false);
+    }
+
+    @Test
+    public void testAdId() throws Exception {
+        // Skip the test if SDK extension 4 is not present.
+        Assume.assumeTrue(AdServicesInfo.INSTANCE.version() >= 4);
+
+        AdIdManagerFutures adIdManager =
+                AdIdManagerFutures.from(ApplicationProvider.getApplicationContext());
+        AdId adId = adIdManager.getAdIdAsync().get();
+        assertThat(adId.getAdId()).isNotEmpty();
+        assertThat(adId.isLimitAdTrackingEnabled()).isNotNull();
+    }
+}
diff --git a/privacysandbox/ads/ads-adservices-java/src/androidTest/java/androidx/privacysandbox/ads/adservices/java/endtoend/measurement/MeasurementManagerTest.java b/privacysandbox/ads/ads-adservices-java/src/androidTest/java/androidx/privacysandbox/ads/adservices/java/endtoend/measurement/MeasurementManagerTest.java
new file mode 100644
index 0000000..2cb7889
--- /dev/null
+++ b/privacysandbox/ads/ads-adservices-java/src/androidTest/java/androidx/privacysandbox/ads/adservices/java/endtoend/measurement/MeasurementManagerTest.java
@@ -0,0 +1,220 @@
+/*
+ * 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.privacysandbox.ads.adservices.java.endtoend.measurement;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import static org.junit.Assert.assertThrows;
+
+import android.net.Uri;
+
+import androidx.privacysandbox.ads.adservices.internal.AdServicesInfo;
+import androidx.privacysandbox.ads.adservices.java.endtoend.TestUtil;
+import androidx.privacysandbox.ads.adservices.java.measurement.MeasurementManagerFutures;
+import androidx.privacysandbox.ads.adservices.measurement.DeletionRequest;
+import androidx.privacysandbox.ads.adservices.measurement.WebSourceParams;
+import androidx.privacysandbox.ads.adservices.measurement.WebSourceRegistrationRequest;
+import androidx.privacysandbox.ads.adservices.measurement.WebTriggerParams;
+import androidx.privacysandbox.ads.adservices.measurement.WebTriggerRegistrationRequest;
+import androidx.test.core.app.ApplicationProvider;
+import androidx.test.filters.SdkSuppress;
+import androidx.test.platform.app.InstrumentationRegistry;
+
+import org.junit.After;
+import org.junit.Assume;
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.junit.runners.JUnit4;
+
+import java.time.Instant;
+import java.util.Collections;
+import java.util.concurrent.ExecutionException;
+import java.util.concurrent.TimeUnit;
+
+@RunWith(JUnit4.class)
+// TODO: Consider refactoring so that we're not duplicating code.
+public class MeasurementManagerTest {
+    private static final String TAG = "MeasurementManagerTest";
+    TestUtil mTestUtil = new TestUtil(InstrumentationRegistry.getInstrumentation(), TAG);
+
+    /* Note: The source and trigger registration used here must match one of those in
+       {@link PreEnrolledAdTechForTest}.
+    */
+    private static final Uri SOURCE_REGISTRATION_URI = Uri.parse("https://test.com/source");
+    private static final Uri TRIGGER_REGISTRATION_URI = Uri.parse("https://test.com/trigger");
+    private static final Uri DESTINATION = Uri.parse("http://trigger-origin.com");
+    private static final Uri OS_DESTINATION = Uri.parse("android-app://com.os.destination");
+    private static final Uri WEB_DESTINATION = Uri.parse("http://web-destination.com");
+    private static final Uri ORIGIN_URI = Uri.parse("https://sample.example1.com");
+    private static final Uri DOMAIN_URI = Uri.parse("https://example2.com");
+
+    private MeasurementManagerFutures mMeasurementManager;
+
+    @Before
+    public void setup() {
+        // To grant access to all pp api app
+        mTestUtil.overrideAllowlists(true);
+        // We need to turn the Consent Manager into debug mode
+        mTestUtil.overrideConsentManagerDebugMode(true);
+        mTestUtil.overrideMeasurementKillSwitches(true);
+        mTestUtil.overrideDisableMeasurementEnrollmentCheck("1");
+        mMeasurementManager =
+                MeasurementManagerFutures.from(ApplicationProvider.getApplicationContext());
+    }
+
+    @After
+    public void tearDown() throws Exception {
+        mTestUtil.overrideAllowlists(false);
+        mTestUtil.overrideConsentManagerDebugMode(false);
+        mTestUtil.resetOverrideDisableMeasurementEnrollmentCheck();
+        mTestUtil.overrideMeasurementKillSwitches(false);
+        mTestUtil.overrideDisableMeasurementEnrollmentCheck("0");
+        // Cool-off rate limiter
+        TimeUnit.SECONDS.sleep(1);
+    }
+
+    @Test
+    public void testRegisterSource_NoServerSetup_NoErrors() throws Exception {
+        // Skip the test if SDK extension 4 is not present.
+        Assume.assumeTrue(AdServicesInfo.INSTANCE.version() >= 4);
+
+        assertThat(mMeasurementManager.registerSourceAsync(
+                SOURCE_REGISTRATION_URI,
+                /* inputEvent= */ null).get())
+                .isNotNull();
+    }
+
+    @Test
+    public void testRegisterTrigger_NoServerSetup_NoErrors() throws Exception {
+        // Skip the test if SDK extension 4 is not present.
+        Assume.assumeTrue(AdServicesInfo.INSTANCE.version() >= 4);
+
+        assertThat(mMeasurementManager.registerTriggerAsync(TRIGGER_REGISTRATION_URI).get())
+                .isNotNull();
+    }
+
+    @Test
+    @SdkSuppress(minSdkVersion = 33)
+    public void registerWebSource_NoErrors() throws Exception {
+        // Skip the test if SDK extension 4 is not present.
+        Assume.assumeTrue(AdServicesInfo.INSTANCE.version() >= 4);
+
+        WebSourceParams webSourceParams =
+                new WebSourceParams(SOURCE_REGISTRATION_URI, false);
+
+        WebSourceRegistrationRequest webSourceRegistrationRequest =
+                new WebSourceRegistrationRequest(
+                        Collections.singletonList(webSourceParams),
+                        SOURCE_REGISTRATION_URI,
+                        /* inputEvent= */ null,
+                        OS_DESTINATION,
+                        WEB_DESTINATION,
+                        /* verifiedDestination= */ null);
+
+        assertThat(mMeasurementManager.registerWebSourceAsync(webSourceRegistrationRequest).get())
+                .isNotNull();
+    }
+
+    @Test
+    @SdkSuppress(minSdkVersion = 33)
+    public void registerWebTrigger_NoErrors() throws Exception {
+        // Skip the test if SDK extension 4 is not present.
+        Assume.assumeTrue(AdServicesInfo.INSTANCE.version() >= 4);
+
+        WebTriggerParams webTriggerParams =
+                new WebTriggerParams(TRIGGER_REGISTRATION_URI, false);
+        WebTriggerRegistrationRequest webTriggerRegistrationRequest =
+                new WebTriggerRegistrationRequest(
+                        Collections.singletonList(webTriggerParams),
+                        DESTINATION);
+
+        assertThat(mMeasurementManager.registerWebTriggerAsync(webTriggerRegistrationRequest).get())
+                .isNotNull();
+    }
+
+    @Test
+    @SdkSuppress(minSdkVersion = 33)
+    public void testDeleteRegistrations_withRequest_withNoRange_withCallback_NoErrors()
+            throws Exception {
+        // Skip the test if SDK extension 4 is not present.
+        Assume.assumeTrue(AdServicesInfo.INSTANCE.version() >= 4);
+
+        DeletionRequest deletionRequest =
+                new DeletionRequest.Builder(
+                        DeletionRequest.DELETION_MODE_ALL,
+                        DeletionRequest.MATCH_BEHAVIOR_DELETE)
+                        .setDomainUris(Collections.singletonList(DOMAIN_URI))
+                        .setOriginUris(Collections.singletonList(ORIGIN_URI))
+                        .build();
+        assertThat(mMeasurementManager.deleteRegistrationsAsync(deletionRequest).get())
+                .isNotNull();
+    }
+
+    @Test
+    @SdkSuppress(minSdkVersion = 33)
+    public void testDeleteRegistrations_withRequest_withEmptyLists_withRange_withCallback_NoErrors()
+            throws Exception {
+        // Skip the test if SDK extension 4 is not present.
+        Assume.assumeTrue(AdServicesInfo.INSTANCE.version() >= 4);
+
+        DeletionRequest deletionRequest =
+                new DeletionRequest.Builder(
+                        DeletionRequest.DELETION_MODE_ALL,
+                        DeletionRequest.MATCH_BEHAVIOR_DELETE)
+                        .setDomainUris(Collections.singletonList(DOMAIN_URI))
+                        .setOriginUris(Collections.singletonList(ORIGIN_URI))
+                        .setStart(Instant.ofEpochMilli(0))
+                        .setEnd(Instant.now())
+                        .build();
+        assertThat(mMeasurementManager.deleteRegistrationsAsync(deletionRequest).get())
+                .isNotNull();
+    }
+
+    @Test
+    @SdkSuppress(minSdkVersion = 33)
+    public void testDeleteRegistrations_withRequest_withInvalidArguments_withCallback_hasError()
+            throws Exception {
+        // Skip the test if SDK extension 4 is not present.
+        Assume.assumeTrue(AdServicesInfo.INSTANCE.version() >= 4);
+
+        DeletionRequest deletionRequest =
+                new DeletionRequest.Builder(
+                        DeletionRequest.DELETION_MODE_ALL,
+                        DeletionRequest.MATCH_BEHAVIOR_DELETE)
+                        .setDomainUris(Collections.singletonList(DOMAIN_URI))
+                        .setOriginUris(Collections.singletonList(ORIGIN_URI))
+                        .setStart(Instant.now().plusMillis(1000))
+                        .setEnd(Instant.now())
+                        .build();
+        Exception exception = assertThrows(
+                ExecutionException.class,
+                () ->
+                mMeasurementManager.deleteRegistrationsAsync(deletionRequest).get());
+        assertThat(exception).hasCauseThat().isInstanceOf(IllegalArgumentException.class);
+    }
+
+    @Test
+    @SdkSuppress(minSdkVersion = 33)
+    public void testMeasurementApiStatus_returnResultStatus() throws Exception {
+        // Skip the test if SDK extension 4 is not present.
+        Assume.assumeTrue(AdServicesInfo.INSTANCE.version() >= 4);
+
+        int result = mMeasurementManager.getMeasurementApiStatusAsync().get();
+        assertThat(result).isEqualTo(1);
+    }
+}
diff --git a/privacysandbox/ads/ads-adservices-java/src/androidTest/java/androidx/privacysandbox/ads/adservices/java/endtoend/topics/TopicsManagerTest.java b/privacysandbox/ads/ads-adservices-java/src/androidTest/java/androidx/privacysandbox/ads/adservices/java/endtoend/topics/TopicsManagerTest.java
new file mode 100644
index 0000000..247232a
--- /dev/null
+++ b/privacysandbox/ads/ads-adservices-java/src/androidTest/java/androidx/privacysandbox/ads/adservices/java/endtoend/topics/TopicsManagerTest.java
@@ -0,0 +1,131 @@
+/*
+ * 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.privacysandbox.ads.adservices.java.endtoend.topics;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import androidx.privacysandbox.ads.adservices.internal.AdServicesInfo;
+import androidx.privacysandbox.ads.adservices.java.endtoend.TestUtil;
+import androidx.privacysandbox.ads.adservices.java.topics.TopicsManagerFutures;
+import androidx.privacysandbox.ads.adservices.topics.GetTopicsRequest;
+import androidx.privacysandbox.ads.adservices.topics.GetTopicsResponse;
+import androidx.privacysandbox.ads.adservices.topics.Topic;
+import androidx.test.core.app.ApplicationProvider;
+import androidx.test.platform.app.InstrumentationRegistry;
+
+import org.junit.After;
+import org.junit.Assume;
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.junit.runners.JUnit4;
+
+import java.util.Arrays;
+
+@RunWith(JUnit4.class)
+// TODO: Consider refactoring so that we're not duplicating code.
+public class TopicsManagerTest {
+    private static final String TAG = "TopicsManagerTest";
+    TestUtil mTestUtil = new TestUtil(InstrumentationRegistry.getInstrumentation(), TAG);
+
+    // Override the Epoch Job Period to this value to speed up the epoch computation.
+    private static final long TEST_EPOCH_JOB_PERIOD_MS = 3000;
+
+    // Default Epoch Period.
+    private static final long TOPICS_EPOCH_JOB_PERIOD_MS = 7 * 86_400_000; // 7 days.
+
+    // Use 0 percent for random topic in the test so that we can verify the returned topic.
+    private static final int TEST_TOPICS_PERCENTAGE_FOR_RANDOM_TOPIC = 0;
+    private static final int TOPICS_PERCENTAGE_FOR_RANDOM_TOPIC = 5;
+
+    @Before
+    public void setup() throws Exception {
+        mTestUtil.overrideKillSwitches(true);
+        // We need to skip 3 epochs so that if there is any usage from other test runs, it will
+        // not be used for epoch retrieval.
+        Thread.sleep(3 * TEST_EPOCH_JOB_PERIOD_MS);
+
+        mTestUtil.overrideEpochPeriod(TEST_EPOCH_JOB_PERIOD_MS);
+        // We need to turn off random topic so that we can verify the returned topic.
+        mTestUtil.overridePercentageForRandomTopic(TEST_TOPICS_PERCENTAGE_FOR_RANDOM_TOPIC);
+        mTestUtil.overrideConsentManagerDebugMode(true);
+        mTestUtil.overrideAllowlists(true);
+        // TODO: Remove this override.
+        mTestUtil.enableEnrollmentCheck(true);
+    }
+
+    @After
+    public void teardown() {
+        mTestUtil.overrideKillSwitches(false);
+        mTestUtil.overrideEpochPeriod(TOPICS_EPOCH_JOB_PERIOD_MS);
+        mTestUtil.overridePercentageForRandomTopic(TOPICS_PERCENTAGE_FOR_RANDOM_TOPIC);
+        mTestUtil.overrideConsentManagerDebugMode(false);
+        mTestUtil.overrideAllowlists(false);
+        mTestUtil.enableEnrollmentCheck(false);
+    }
+
+    @Test
+    public void testTopicsManager_runClassifier() throws Exception {
+        // Skip the test if SDK extension 4 is not present.
+        Assume.assumeTrue(AdServicesInfo.INSTANCE.version() >= 4);
+
+        TopicsManagerFutures topicsManager =
+                TopicsManagerFutures.from(ApplicationProvider.getApplicationContext());
+        GetTopicsRequest request = new GetTopicsRequest.Builder()
+                .setSdkName("sdk1")
+                .setShouldRecordObservation(true)
+                .build();
+        GetTopicsResponse response = topicsManager.getTopicsAsync(request).get();
+
+        // At beginning, Sdk1 receives no topic.
+        assertThat(response.getTopics().isEmpty());
+
+        // Now force the Epoch Computation Job. This should be done in the same epoch for
+        // callersCanLearnMap to have the entry for processing.
+        mTestUtil.forceEpochComputationJob();
+
+        // Wait to the next epoch. We will not need to do this after we implement the fix in
+        // go/rb-topics-epoch-scheduling
+        Thread.sleep(TEST_EPOCH_JOB_PERIOD_MS);
+
+        // Since the sdk1 called the Topics API in the previous Epoch, it should receive some topic.
+        response = topicsManager.getTopicsAsync(request).get();
+        assertThat(response.getTopics()).isNotEmpty();
+
+        // Top 5 classifications for empty string with v2 model are [10230, 10253, 10227, 10250,
+        // 10257]. This is computed by running the model on the device for empty string.
+        // These 5 classification topics will become top 5 topics of the epoch since there is
+        // no other apps calling Topics API.
+        // The app will be assigned one random topic from one of these 5 topics.
+        assertThat(response.getTopics()).hasSize(1);
+
+        Topic topic = response.getTopics().get(0);
+
+        // topic is one of the 5 classification topics of the Test App.
+        assertThat(topic.getTopicId()).isIn(Arrays.asList(10230, 10253, 10227, 10250, 10257));
+
+        assertThat(topic.getModelVersion()).isAtLeast(1L);
+        assertThat(topic.getTaxonomyVersion()).isAtLeast(1L);
+
+        // Sdk 2 did not call getTopics API. So it should not receive any topic.
+        GetTopicsResponse response2 = topicsManager.getTopicsAsync(
+                new GetTopicsRequest.Builder()
+                        .setSdkName("sdk2")
+                        .build()).get();
+        assertThat(response2.getTopics()).isEmpty();
+    }
+}
diff --git a/privacysandbox/ads/ads-adservices-java/src/androidTest/java/androidx/privacysandbox/ads/adservices/java/measurement/MeasurementManagerFuturesTest.kt b/privacysandbox/ads/ads-adservices-java/src/androidTest/java/androidx/privacysandbox/ads/adservices/java/measurement/MeasurementManagerFuturesTest.kt
new file mode 100644
index 0000000..e4bdbb1
--- /dev/null
+++ b/privacysandbox/ads/ads-adservices-java/src/androidTest/java/androidx/privacysandbox/ads/adservices/java/measurement/MeasurementManagerFuturesTest.kt
@@ -0,0 +1,299 @@
+/*
+ * 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.privacysandbox.ads.adservices.java.measurement
+
+import android.adservices.measurement.MeasurementManager
+import android.content.Context
+import android.net.Uri
+import android.os.OutcomeReceiver
+import android.os.ext.SdkExtensions
+import android.view.InputEvent
+import androidx.annotation.RequiresExtension
+import androidx.privacysandbox.ads.adservices.java.measurement.MeasurementManagerFutures.Companion.from
+import androidx.privacysandbox.ads.adservices.measurement.DeletionRequest
+import androidx.privacysandbox.ads.adservices.measurement.WebSourceParams
+import androidx.privacysandbox.ads.adservices.measurement.WebSourceRegistrationRequest
+import androidx.privacysandbox.ads.adservices.measurement.WebTriggerParams
+import androidx.privacysandbox.ads.adservices.measurement.WebTriggerRegistrationRequest
+import androidx.test.core.app.ApplicationProvider
+import androidx.test.ext.junit.runners.AndroidJUnit4
+import androidx.test.filters.SdkSuppress
+import androidx.test.filters.SmallTest
+import com.google.common.truth.Truth.assertThat
+import java.time.Instant
+import org.junit.Assume
+import org.junit.Before
+import org.junit.Test
+import org.junit.runner.RunWith
+import org.mockito.ArgumentCaptor
+import org.mockito.ArgumentMatchers.any
+import org.mockito.Mockito.doAnswer
+import org.mockito.Mockito.mock
+import org.mockito.Mockito.spy
+import org.mockito.Mockito.verify
+import org.mockito.Mockito.`when`
+import org.mockito.invocation.InvocationOnMock
+
+@SmallTest
+@SuppressWarnings("NewApi")
+@RunWith(AndroidJUnit4::class)
+@SdkSuppress(minSdkVersion = 30)
+class MeasurementManagerFuturesTest {
+
+    @Before
+    fun setUp() {
+        mContext = spy(ApplicationProvider.getApplicationContext<Context>())
+    }
+
+    @Test
+    @SdkSuppress(maxSdkVersion = 33, minSdkVersion = 30)
+    fun testMeasurementOlderVersions() {
+        val sdkExtVersion = SdkExtensions.getExtensionVersion(SdkExtensions.AD_SERVICES)
+
+        Assume.assumeTrue("maxSdkVersion = API 33 ext 3", sdkExtVersion < 4)
+        assertThat(from(mContext)).isEqualTo(null)
+    }
+
+    @Test
+    @RequiresExtension(extension = SdkExtensions.AD_SERVICES, version = 4)
+    fun testDeleteRegistrationsAsync() {
+        val sdkExtVersion = SdkExtensions.getExtensionVersion(SdkExtensions.AD_SERVICES)
+
+        Assume.assumeTrue("minSdkVersion = API 33 ext 4", sdkExtVersion >= 4)
+        val measurementManager = mockMeasurementManager(mContext)
+        val managerCompat = from(mContext)
+
+        // Set up the request.
+        val answer = { args: InvocationOnMock ->
+            val receiver = args.getArgument<OutcomeReceiver<Any, Exception>>(2)
+            receiver.onResult(Object())
+            null
+        }
+        doAnswer(answer).`when`(measurementManager).deleteRegistrations(any(), any(), any())
+
+        // Actually invoke the compat code.
+        val request = DeletionRequest(
+            DeletionRequest.DELETION_MODE_ALL,
+            DeletionRequest.MATCH_BEHAVIOR_DELETE,
+            Instant.now(),
+            Instant.now(),
+            listOf(uri1),
+            listOf(uri1))
+
+        managerCompat!!.deleteRegistrationsAsync(request).get()
+
+        // Verify that the compat code was invoked correctly.
+        val captor = ArgumentCaptor.forClass(
+            android.adservices.measurement.DeletionRequest::class.java
+        )
+        verify(measurementManager).deleteRegistrations(captor.capture(), any(), any())
+
+        // Verify that the request that the compat code makes to the platform is correct.
+        verifyDeletionRequest(captor.value)
+    }
+
+    @Test
+    @RequiresExtension(extension = SdkExtensions.AD_SERVICES, version = 4)
+    fun testRegisterSourceAsync() {
+        val sdkExtVersion = SdkExtensions.getExtensionVersion(SdkExtensions.AD_SERVICES)
+
+        Assume.assumeTrue("minSdkVersion = API 33 ext 4", sdkExtVersion >= 4)
+        val inputEvent = mock(InputEvent::class.java)
+        val measurementManager = mockMeasurementManager(mContext)
+        val managerCompat = from(mContext)
+        val answer = { args: InvocationOnMock ->
+            val receiver = args.getArgument<OutcomeReceiver<Any, Exception>>(3)
+            receiver.onResult(Object())
+            null
+        }
+        doAnswer(answer).`when`(measurementManager).registerSource(any(), any(), any(), any())
+
+        // Actually invoke the compat code.
+        managerCompat!!.registerSourceAsync(uri1, inputEvent).get()
+
+        // Verify that the compat code was invoked correctly.
+        val captor1 = ArgumentCaptor.forClass(Uri::class.java)
+        val captor2 = ArgumentCaptor.forClass(InputEvent::class.java)
+        verify(measurementManager).registerSource(
+            captor1.capture(),
+            captor2.capture(),
+            any(),
+            any())
+
+        // Verify that the request that the compat code makes to the platform is correct.
+        assertThat(captor1.value == uri1)
+        assertThat(captor2.value == inputEvent)
+    }
+
+    @Test
+    @RequiresExtension(extension = SdkExtensions.AD_SERVICES, version = 4)
+    fun testRegisterTriggerAsync() {
+        val sdkExtVersion = SdkExtensions.getExtensionVersion(SdkExtensions.AD_SERVICES)
+
+        Assume.assumeTrue("minSdkVersion = API 33 ext 4", sdkExtVersion >= 4)
+        val measurementManager = mockMeasurementManager(mContext)
+        val managerCompat = from(mContext)
+        val answer = { args: InvocationOnMock ->
+            val receiver = args.getArgument<OutcomeReceiver<Any, Exception>>(2)
+            receiver.onResult(Object())
+            null
+        }
+        doAnswer(answer).`when`(measurementManager).registerTrigger(any(), any(), any())
+
+        // Actually invoke the compat code.
+        managerCompat!!.registerTriggerAsync(uri1).get()
+
+        // Verify that the compat code was invoked correctly.
+        val captor1 = ArgumentCaptor.forClass(Uri::class.java)
+        verify(measurementManager).registerTrigger(
+            captor1.capture(),
+            any(),
+            any())
+
+        // Verify that the request that the compat code makes to the platform is correct.
+        assertThat(captor1.value == uri1)
+    }
+
+    @Test
+    @RequiresExtension(extension = SdkExtensions.AD_SERVICES, version = 4)
+    fun testRegisterWebSourceAsync() {
+        val sdkExtVersion = SdkExtensions.getExtensionVersion(SdkExtensions.AD_SERVICES)
+
+        Assume.assumeTrue("minSdkVersion = API 33 ext 4", sdkExtVersion >= 4)
+        val measurementManager = mockMeasurementManager(mContext)
+        val managerCompat = from(mContext)
+        val answer = { args: InvocationOnMock ->
+            val receiver = args.getArgument<OutcomeReceiver<Any, Exception>>(2)
+            receiver.onResult(Object())
+            null
+        }
+        doAnswer(answer).`when`(measurementManager).registerWebSource(any(), any(), any())
+
+        val request = WebSourceRegistrationRequest.Builder(
+            listOf(WebSourceParams(uri2, false)), uri1)
+            .setAppDestination(uri1)
+            .build()
+
+        // Actually invoke the compat code.
+        managerCompat!!.registerWebSourceAsync(request).get()
+
+        // Verify that the compat code was invoked correctly.
+        val captor1 = ArgumentCaptor.forClass(
+            android.adservices.measurement.WebSourceRegistrationRequest::class.java)
+        verify(measurementManager).registerWebSource(
+            captor1.capture(),
+            any(),
+            any())
+
+        // Verify that the request that the compat code makes to the platform is correct.
+        val actualRequest = captor1.value
+        assertThat(actualRequest.topOriginUri == uri1)
+        assertThat(actualRequest.sourceParams.size == 1)
+        assertThat(actualRequest.sourceParams[0].registrationUri == uri2)
+        assertThat(!actualRequest.sourceParams[0].isDebugKeyAllowed)
+    }
+
+    @Test
+    @RequiresExtension(extension = SdkExtensions.AD_SERVICES, version = 4)
+    fun testRegisterWebTriggerAsync() {
+        val sdkExtVersion = SdkExtensions.getExtensionVersion(SdkExtensions.AD_SERVICES)
+
+        Assume.assumeTrue("minSdkVersion = API 33 ext 4", sdkExtVersion >= 4)
+        val measurementManager = mockMeasurementManager(mContext)
+        val managerCompat = from(mContext)
+        val answer = { args: InvocationOnMock ->
+            val receiver = args.getArgument<OutcomeReceiver<Any, Exception>>(2)
+            receiver.onResult(Object())
+            null
+        }
+        doAnswer(answer).`when`(measurementManager).registerWebTrigger(any(), any(), any())
+
+        val request = WebTriggerRegistrationRequest(listOf(WebTriggerParams(uri1, false)), uri2)
+
+        // Actually invoke the compat code.
+        managerCompat!!.registerWebTriggerAsync(request).get()
+
+        // Verify that the compat code was invoked correctly.
+        val captor1 = ArgumentCaptor.forClass(
+            android.adservices.measurement.WebTriggerRegistrationRequest::class.java)
+        verify(measurementManager).registerWebTrigger(
+            captor1.capture(),
+            any(),
+            any())
+
+        // Verify that the request that the compat code makes to the platform is correct.
+        val actualRequest = captor1.value
+        assertThat(actualRequest.destination == uri2)
+        assertThat(actualRequest.triggerParams.size == 1)
+        assertThat(actualRequest.triggerParams[0].registrationUri == uri1)
+        assertThat(!actualRequest.triggerParams[0].isDebugKeyAllowed)
+    }
+
+    @Test
+    @RequiresExtension(extension = SdkExtensions.AD_SERVICES, version = 4)
+    fun testMeasurementApiStatusAsync() {
+        val sdkExtVersion = SdkExtensions.getExtensionVersion(SdkExtensions.AD_SERVICES)
+
+        Assume.assumeTrue("minSdkVersion = API 33 ext 4", sdkExtVersion >= 4)
+        val measurementManager = mockMeasurementManager(mContext)
+        val managerCompat = from(mContext)
+        val state = MeasurementManager.MEASUREMENT_API_STATE_DISABLED
+        val answer = { args: InvocationOnMock ->
+            val receiver = args.getArgument<OutcomeReceiver<Int, Exception>>(1)
+            receiver.onResult(state)
+            null
+        }
+        doAnswer(answer).`when`(measurementManager).getMeasurementApiStatus(any(), any())
+
+        // Actually invoke the compat code.
+        val result = managerCompat!!.getMeasurementApiStatusAsync()
+        result.get()
+
+        // Verify that the compat code was invoked correctly.
+        verify(measurementManager).getMeasurementApiStatus(any(), any())
+
+        // Verify that the result.
+        assertThat(result.get() == state)
+    }
+
+    @SdkSuppress(minSdkVersion = 30)
+    @RequiresExtension(extension = SdkExtensions.AD_SERVICES, version = 4)
+    companion object {
+
+        private val uri1: Uri = Uri.parse("www.abc.com")
+        private val uri2: Uri = Uri.parse("http://www.xyz.com")
+        private lateinit var mContext: Context
+
+        private fun mockMeasurementManager(spyContext: Context): MeasurementManager {
+            val measurementManager = mock(MeasurementManager::class.java)
+            `when`(spyContext.getSystemService(MeasurementManager::class.java))
+                .thenReturn(measurementManager)
+            return measurementManager
+        }
+
+        private fun verifyDeletionRequest(request: android.adservices.measurement.DeletionRequest) {
+            // Set up the request that we expect the compat code to invoke.
+            val expectedRequest = android.adservices.measurement.DeletionRequest.Builder()
+                .setDomainUris(listOf(uri1))
+                .setOriginUris(listOf(uri1))
+                .build()
+
+            assertThat(HashSet(request.domainUris) == HashSet(expectedRequest.domainUris))
+            assertThat(HashSet(request.originUris) == HashSet(expectedRequest.originUris))
+        }
+    }
+}
\ No newline at end of file
diff --git a/privacysandbox/ads/ads-adservices-java/src/androidTest/java/androidx/privacysandbox/ads/adservices/java/topics/TopicsManagerFuturesTest.kt b/privacysandbox/ads/ads-adservices-java/src/androidTest/java/androidx/privacysandbox/ads/adservices/java/topics/TopicsManagerFuturesTest.kt
new file mode 100644
index 0000000..36ce0d6
--- /dev/null
+++ b/privacysandbox/ads/ads-adservices-java/src/androidTest/java/androidx/privacysandbox/ads/adservices/java/topics/TopicsManagerFuturesTest.kt
@@ -0,0 +1,155 @@
+/*
+ * 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.privacysandbox.ads.adservices.java.topics
+
+import android.adservices.topics.Topic
+import android.adservices.topics.TopicsManager
+import android.content.Context
+import android.os.OutcomeReceiver
+import android.os.ext.SdkExtensions
+import androidx.annotation.RequiresExtension
+import androidx.privacysandbox.ads.adservices.topics.GetTopicsRequest
+import androidx.privacysandbox.ads.adservices.topics.GetTopicsResponse
+import androidx.privacysandbox.ads.adservices.java.topics.TopicsManagerFutures.Companion.from
+import androidx.test.core.app.ApplicationProvider
+import androidx.test.ext.junit.runners.AndroidJUnit4
+import androidx.test.filters.SdkSuppress
+import androidx.test.filters.SmallTest
+import com.google.common.util.concurrent.ListenableFuture
+import com.google.common.truth.Truth.assertThat
+import org.junit.Assert
+import org.junit.Assume
+import org.junit.Before
+import org.junit.Test
+import org.junit.runner.RunWith
+import org.mockito.ArgumentCaptor
+import org.mockito.ArgumentMatchers.any
+import org.mockito.Mockito.doAnswer
+import org.mockito.Mockito.mock
+import org.mockito.Mockito.spy
+import org.mockito.Mockito.verify
+import org.mockito.Mockito.`when`
+import org.mockito.invocation.InvocationOnMock
+
+@SmallTest
+@SuppressWarnings("NewApi")
+@RunWith(AndroidJUnit4::class)
+@SdkSuppress(minSdkVersion = 30)
+class TopicsManagerFuturesTest {
+
+    @Before
+    fun setUp() {
+        mContext = spy(ApplicationProvider.getApplicationContext<Context>())
+    }
+
+    @Test
+    @SdkSuppress(maxSdkVersion = 33, minSdkVersion = 30)
+    fun testTopicsOlderVersions() {
+        val sdkExtVersion = SdkExtensions.getExtensionVersion(SdkExtensions.AD_SERVICES)
+
+        Assume.assumeTrue("maxSdkVersion = API 33 ext 3", sdkExtVersion < 4)
+        assertThat(from(mContext)).isEqualTo(null)
+    }
+
+    @Test
+    @SuppressWarnings("NewApi")
+    @RequiresExtension(extension = SdkExtensions.AD_SERVICES, version = 4)
+    fun testTopicsAsync() {
+        val sdkExtVersion = SdkExtensions.getExtensionVersion(SdkExtensions.AD_SERVICES)
+
+        Assume.assumeTrue("minSdkVersion = API 33 ext 4", sdkExtVersion >= 4)
+        val topicsManager = mockTopicsManager(mContext)
+        setupTopicsResponse(topicsManager)
+        val managerCompat = from(mContext)
+
+        // Actually invoke the compat code.
+        val request = GetTopicsRequest.Builder()
+            .setSdkName(mSdkName)
+            .setShouldRecordObservation(true)
+            .build()
+
+        val result: ListenableFuture<GetTopicsResponse> =
+            managerCompat!!.getTopicsAsync(request)
+
+        // Verify that the result of the compat call is correct.
+        verifyResponse(result.get())
+
+        // Verify that the compat code was invoked correctly.
+        val captor = ArgumentCaptor.forClass(android.adservices.topics.GetTopicsRequest::class.java)
+        verify(topicsManager).getTopics(captor.capture(), any(), any())
+
+        // Verify that the request that the compat code makes to the platform is correct.
+        verifyRequest(captor.value)
+    }
+
+    companion object {
+        private lateinit var mContext: Context
+        private val mSdkName: String = "sdk1"
+
+        @RequiresExtension(extension = SdkExtensions.AD_SERVICES, version = 4)
+        private fun mockTopicsManager(spyContext: Context): TopicsManager {
+            val topicsManager = mock(TopicsManager::class.java)
+            `when`(spyContext.getSystemService(TopicsManager::class.java))
+                .thenReturn(topicsManager)
+            return topicsManager
+        }
+
+        @RequiresExtension(extension = SdkExtensions.AD_SERVICES, version = 4)
+        private fun setupTopicsResponse(topicsManager: TopicsManager) {
+            // Set up the response that TopicsManager will return when the compat code calls it.
+            val topic1 = Topic(1, 1, 1)
+            val topic2 = Topic(2, 2, 2)
+            val topics = listOf(topic1, topic2)
+            val response = android.adservices.topics.GetTopicsResponse.Builder(topics).build()
+            val answer = { args: InvocationOnMock ->
+                val receiver = args.getArgument<
+                    OutcomeReceiver<android.adservices.topics.GetTopicsResponse, Exception>>(2)
+                receiver.onResult(response)
+                null
+            }
+            doAnswer(answer)
+                .`when`(topicsManager).getTopics(
+                    any(),
+                    any(),
+                    any()
+                )
+        }
+
+        @RequiresExtension(extension = SdkExtensions.AD_SERVICES, version = 4)
+        private fun verifyRequest(topicsRequest: android.adservices.topics.GetTopicsRequest) {
+            // Set up the request that we expect the compat code to invoke.
+            val expectedRequest = android.adservices.topics.GetTopicsRequest.Builder()
+                .setAdsSdkName(mSdkName)
+                .build()
+
+            Assert.assertEquals(expectedRequest.adsSdkName, topicsRequest.adsSdkName)
+        }
+
+        @RequiresExtension(extension = SdkExtensions.AD_SERVICES, version = 4)
+        private fun verifyResponse(getTopicsResponse: GetTopicsResponse) {
+            Assert.assertEquals(2, getTopicsResponse.topics.size)
+            val topic1 = getTopicsResponse.topics[0]
+            val topic2 = getTopicsResponse.topics[1]
+            Assert.assertEquals(1, topic1.topicId)
+            Assert.assertEquals(1, topic1.modelVersion)
+            Assert.assertEquals(1, topic1.taxonomyVersion)
+            Assert.assertEquals(2, topic2.topicId)
+            Assert.assertEquals(2, topic2.modelVersion)
+            Assert.assertEquals(2, topic2.taxonomyVersion)
+        }
+    }
+}
\ No newline at end of file
diff --git a/privacysandbox/ads/ads-adservices-java/src/androidTest/res/xml/ad_services_config.xml b/privacysandbox/ads/ads-adservices-java/src/androidTest/res/xml/ad_services_config.xml
new file mode 100644
index 0000000..154098e
--- /dev/null
+++ b/privacysandbox/ads/ads-adservices-java/src/androidTest/res/xml/ad_services_config.xml
@@ -0,0 +1,21 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+  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.
+  -->
+<ad-services-config>
+    <topics allowAllToAccess="true" />
+    <custom-audiences allowAllToAccess="true" />
+    <attribution allowAllToAccess="true" />
+</ad-services-config>
\ No newline at end of file
diff --git a/privacysandbox/ads/ads-adservices-java/src/main/java/androidx/privacysandbox/ads/adservices/java/adid/AdIdManagerFutures.kt b/privacysandbox/ads/ads-adservices-java/src/main/java/androidx/privacysandbox/ads/adservices/java/adid/AdIdManagerFutures.kt
new file mode 100644
index 0000000..07b112b
--- /dev/null
+++ b/privacysandbox/ads/ads-adservices-java/src/main/java/androidx/privacysandbox/ads/adservices/java/adid/AdIdManagerFutures.kt
@@ -0,0 +1,71 @@
+/*
+ * 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.privacysandbox.ads.adservices.java.adid
+
+import androidx.privacysandbox.ads.adservices.java.internal.asListenableFuture
+import android.adservices.common.AdServicesPermissions
+import android.content.Context
+import android.os.LimitExceededException
+import androidx.annotation.DoNotInline
+import androidx.annotation.RequiresPermission
+import androidx.privacysandbox.ads.adservices.adid.AdId
+import androidx.privacysandbox.ads.adservices.adid.AdIdManager
+import com.google.common.util.concurrent.ListenableFuture
+import kotlinx.coroutines.CoroutineScope
+import kotlinx.coroutines.Dispatchers
+import kotlinx.coroutines.async
+
+/**
+ * AdId Manager provides APIs for app and ad-SDKs to access advertising ID. The advertising ID is a
+ * unique, per-device, user-resettable ID for advertising. It gives users better controls and
+ * provides developers with a simple, standard system to continue to monetize their apps via
+ * personalized ads (formerly known as interest-based ads). This class can be used by Java clients.
+ */
+abstract class AdIdManagerFutures internal constructor() {
+    /**
+     * Return the AdId.
+     *
+     * @throws SecurityException if caller is not authorized to call this API.
+     * @throws IllegalStateException if this API is not available.
+     * @throws LimitExceededException if rate limit was reached.
+     */
+    @RequiresPermission(AdServicesPermissions.ACCESS_ADSERVICES_AD_ID)
+    abstract fun getAdIdAsync(): ListenableFuture<AdId>
+
+    private class Api33Ext4JavaImpl(private val mAdIdManager: AdIdManager) : AdIdManagerFutures() {
+        @DoNotInline
+        @RequiresPermission(AdServicesPermissions.ACCESS_ADSERVICES_AD_ID)
+        override fun getAdIdAsync(): ListenableFuture<AdId> {
+            return CoroutineScope(Dispatchers.Main).async {
+                mAdIdManager.getAdId()
+            }.asListenableFuture()
+        }
+    }
+
+    companion object {
+        /**
+         *  Creates [AdIdManagerFutures].
+         *
+         *  @return AdIdManagerFutures object. If the device is running an incompatible
+         *  build, the value returned is null.
+         */
+        @JvmStatic
+        fun from(context: Context): AdIdManagerFutures? {
+            return AdIdManager.obtain(context)?.let { Api33Ext4JavaImpl(it) }
+        }
+    }
+}
\ No newline at end of file
diff --git a/privacysandbox/ads/ads-adservices-java/src/main/java/androidx/privacysandbox/ads/adservices/java/adselection/AdSelectionManagerFutures.kt b/privacysandbox/ads/ads-adservices-java/src/main/java/androidx/privacysandbox/ads/adservices/java/adselection/AdSelectionManagerFutures.kt
new file mode 100644
index 0000000..4726167
--- /dev/null
+++ b/privacysandbox/ads/ads-adservices-java/src/main/java/androidx/privacysandbox/ads/adservices/java/adselection/AdSelectionManagerFutures.kt
@@ -0,0 +1,121 @@
+/*
+ * 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.privacysandbox.ads.adservices.java.adselection
+
+import androidx.privacysandbox.ads.adservices.java.internal.asListenableFuture
+import androidx.privacysandbox.ads.adservices.adselection.AdSelectionManager
+import androidx.privacysandbox.ads.adservices.adselection.AdSelectionManager.Companion.obtain
+import android.adservices.common.AdServicesPermissions
+import android.content.Context
+import android.os.LimitExceededException
+import android.os.TransactionTooLargeException
+import androidx.annotation.DoNotInline
+import androidx.annotation.RequiresPermission
+import androidx.privacysandbox.ads.adservices.adselection.AdSelectionConfig
+import androidx.privacysandbox.ads.adservices.adselection.AdSelectionOutcome
+import androidx.privacysandbox.ads.adservices.adselection.ReportImpressionRequest
+import com.google.common.util.concurrent.ListenableFuture
+import java.util.concurrent.TimeoutException
+import kotlinx.coroutines.CoroutineScope
+import kotlinx.coroutines.Dispatchers
+import kotlinx.coroutines.async
+
+/**
+ * This class provides APIs to select ads and report impressions.
+ * This class can be used by Java clients.
+ */
+abstract class AdSelectionManagerFutures internal constructor() {
+
+    /**
+     * Runs the ad selection process on device to select a remarketing ad for the caller
+     * application.
+     *
+     * @param adSelectionConfig the config The input {@code adSelectionConfig} is provided by the
+     * Ads SDK and the [AdSelectionConfig] object is transferred via a Binder call. For this
+     * reason, the total size of these objects is bound to the Android IPC limitations. Failures to
+     * transfer the [AdSelectionConfig] will throws an [TransactionTooLargeException].
+     *
+     * The output is passed by the receiver, which either returns an [AdSelectionOutcome]
+     * for a successful run, or an [Exception] includes the type of the exception thrown and
+     * the corresponding error message.
+     *
+     * If the [IllegalArgumentException] is thrown, it is caused by invalid input argument
+     * the API received to run the ad selection.
+     *
+     * If the [IllegalStateException] is thrown with error message "Failure of AdSelection
+     * services.", it is caused by an internal failure of the ad selection service.
+     *
+     * If the [TimeoutException] is thrown, it is caused when a timeout is encountered
+     * during bidding, scoring, or overall selection process to find winning Ad.
+     *
+     * If the [LimitExceededException] is thrown, it is caused when the calling package
+     * exceeds the allowed rate limits and is throttled.
+     */
+    @RequiresPermission(AdServicesPermissions.ACCESS_ADSERVICES_CUSTOM_AUDIENCE)
+    abstract fun selectAdsAsync(
+        adSelectionConfig: AdSelectionConfig
+    ): ListenableFuture<AdSelectionOutcome>
+
+    /**
+     * Report the given impression. The [ReportImpressionRequest] is provided by the Ads SDK.
+     * The receiver either returns a {@code void} for a successful run, or an [Exception]
+     * indicates the error.
+     *
+     * @param reportImpressionRequest the request for reporting impression.
+     */
+    @RequiresPermission(AdServicesPermissions.ACCESS_ADSERVICES_CUSTOM_AUDIENCE)
+    abstract fun reportImpressionAsync(
+        reportImpressionRequest: ReportImpressionRequest
+    ): ListenableFuture<Unit>
+
+    private class Api33Ext4JavaImpl(
+        private val mAdSelectionManager: AdSelectionManager?
+    ) : AdSelectionManagerFutures() {
+        @DoNotInline
+        @RequiresPermission(AdServicesPermissions.ACCESS_ADSERVICES_CUSTOM_AUDIENCE)
+        override fun selectAdsAsync(
+            adSelectionConfig: AdSelectionConfig
+        ): ListenableFuture<AdSelectionOutcome> {
+            return CoroutineScope(Dispatchers.Default).async {
+                mAdSelectionManager!!.selectAds(adSelectionConfig)
+            }.asListenableFuture()
+        }
+
+        @DoNotInline
+        @RequiresPermission(AdServicesPermissions.ACCESS_ADSERVICES_CUSTOM_AUDIENCE)
+        override fun reportImpressionAsync(
+            reportImpressionRequest: ReportImpressionRequest
+        ): ListenableFuture<Unit> {
+            return CoroutineScope(Dispatchers.Default).async {
+                mAdSelectionManager!!.reportImpression(reportImpressionRequest)
+            }.asListenableFuture()
+        }
+    }
+
+    companion object {
+        /**
+         *  Creates [AdSelectionManagerFutures].
+         *
+         *  @return AdSelectionManagerFutures object. If the device is running an incompatible
+         *  build, the value returned is null.
+         */
+        @JvmStatic
+        fun from(context: Context): AdSelectionManagerFutures? {
+            return obtain(context)?.let { Api33Ext4JavaImpl(it) }
+        }
+    }
+}
\ No newline at end of file
diff --git a/privacysandbox/ads/ads-adservices-java/src/main/java/androidx/privacysandbox/ads/adservices/java/appsetid/AppSetIdManagerFutures.kt b/privacysandbox/ads/ads-adservices-java/src/main/java/androidx/privacysandbox/ads/adservices/java/appsetid/AppSetIdManagerFutures.kt
new file mode 100644
index 0000000..f0812fd
--- /dev/null
+++ b/privacysandbox/ads/ads-adservices-java/src/main/java/androidx/privacysandbox/ads/adservices/java/appsetid/AppSetIdManagerFutures.kt
@@ -0,0 +1,67 @@
+/*
+ * 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.privacysandbox.ads.adservices.java.appsetid
+
+import androidx.privacysandbox.ads.adservices.java.internal.asListenableFuture
+import android.content.Context
+import android.os.LimitExceededException
+import androidx.annotation.DoNotInline
+import androidx.privacysandbox.ads.adservices.appsetid.AppSetId
+import androidx.privacysandbox.ads.adservices.appsetid.AppSetIdManager
+import com.google.common.util.concurrent.ListenableFuture
+import kotlinx.coroutines.CoroutineScope
+import kotlinx.coroutines.Dispatchers
+import kotlinx.coroutines.async
+
+/**
+ * AppSetIdManager provides APIs for app and ad-SDKs to access appSetId for non-monetizing purpose.
+ * This class can be used by Java clients.
+ */
+abstract class AppSetIdManagerFutures internal constructor() {
+    /**
+     * Return the AppSetId.
+     *
+     * @throws SecurityException if caller is not authorized to call this API.
+     * @throws IllegalStateException if this API is not available.
+     * @throws LimitExceededException if rate limit was reached.
+     */
+    abstract fun getAppSetIdAsync(): ListenableFuture<AppSetId>
+
+    private class Api33Ext4JavaImpl(
+        private val mAppSetIdManager: AppSetIdManager
+    ) : AppSetIdManagerFutures() {
+        @DoNotInline
+        override fun getAppSetIdAsync(): ListenableFuture<AppSetId> {
+            return CoroutineScope(Dispatchers.Main).async {
+                mAppSetIdManager.getAppSetId()
+            }.asListenableFuture()
+        }
+    }
+
+    companion object {
+        /**
+         *  Creates [AppSetIdManagerFutures].
+         *
+         *  @return AppSetIdManagerFutures object. If the device is running an incompatible
+         *  build, the value returned is null.
+         */
+        @JvmStatic
+        fun from(context: Context): AppSetIdManagerFutures? {
+            return AppSetIdManager.obtain(context)?.let { Api33Ext4JavaImpl(it) }
+        }
+    }
+}
\ No newline at end of file
diff --git a/privacysandbox/ads/ads-adservices-java/src/main/java/androidx/privacysandbox/ads/adservices/java/customaudience/CustomAudienceManagerFutures.kt b/privacysandbox/ads/ads-adservices-java/src/main/java/androidx/privacysandbox/ads/adservices/java/customaudience/CustomAudienceManagerFutures.kt
new file mode 100644
index 0000000..cb43cf4
--- /dev/null
+++ b/privacysandbox/ads/ads-adservices-java/src/main/java/androidx/privacysandbox/ads/adservices/java/customaudience/CustomAudienceManagerFutures.kt
@@ -0,0 +1,140 @@
+/*
+ * 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.privacysandbox.ads.adservices.java.customaudience
+
+import androidx.privacysandbox.ads.adservices.java.internal.asListenableFuture
+import androidx.privacysandbox.ads.adservices.customaudience.CustomAudience
+import androidx.privacysandbox.ads.adservices.customaudience.CustomAudienceManager
+import androidx.privacysandbox.ads.adservices.customaudience.CustomAudienceManager.Companion.obtain
+import android.adservices.common.AdServicesPermissions
+import android.content.Context
+import android.os.LimitExceededException
+import androidx.annotation.DoNotInline
+import androidx.annotation.RequiresPermission
+import androidx.privacysandbox.ads.adservices.customaudience.JoinCustomAudienceRequest
+import androidx.privacysandbox.ads.adservices.customaudience.LeaveCustomAudienceRequest
+import com.google.common.util.concurrent.ListenableFuture
+import kotlinx.coroutines.CoroutineScope
+import kotlinx.coroutines.Dispatchers
+import kotlinx.coroutines.async
+
+/**
+ * This class provides APIs for app and ad-SDKs to join / leave custom audiences.
+ * This class can be used by Java clients.
+ */
+abstract class CustomAudienceManagerFutures internal constructor() {
+
+    /**
+     * Adds the user to the given [CustomAudience].
+     *
+     * An attempt to register the user for a custom audience with the same combination of {@code
+     * ownerPackageName}, {@code buyer}, and {@code name} will cause the existing custom audience's
+     * information to be overwritten, including the list of ads data.
+     *
+     * Note that the ads list can be completely overwritten by the daily background fetch job.
+     *
+     * This call fails with an [SecurityException] if
+     *
+     * <ol>
+     *   <li>the {@code ownerPackageName} is not calling app's package name and/or
+     *   <li>the buyer is not authorized to use the API.
+     * </ol>
+     *
+     * This call fails with an [IllegalArgumentException] if
+     *
+     * <ol>
+     *   <li>the storage limit has been exceeded by the calling application and/or
+     *   <li>any URI parameters in the [CustomAudience] given are not authenticated with the
+     *       [CustomAudience] buyer.
+     * </ol>
+     *
+     * This call fails with [LimitExceededException] if the calling package exceeds the
+     * allowed rate limits and is throttled.
+     *
+     * This call fails with an [IllegalStateException] if an internal service error is
+     * encountered.
+     *
+     * @param request The request to join custom audience.
+     */
+    @RequiresPermission(AdServicesPermissions.ACCESS_ADSERVICES_CUSTOM_AUDIENCE)
+    abstract fun joinCustomAudienceAsync(
+        request: JoinCustomAudienceRequest
+    ): ListenableFuture<Unit>
+
+    /**
+     * Attempts to remove a user from a custom audience by deleting any existing
+     * [CustomAudience] data, identified by {@code ownerPackageName}, {@code buyer}, and {@code
+     * name}.
+     *
+     * This call fails with an [SecurityException] if
+     *
+     * <ol>
+     *   <li>the {@code ownerPackageName} is not calling app's package name; and/or
+     *   <li>the buyer is not authorized to use the API.
+     * </ol>
+     *
+     * This call fails with [LimitExceededException] if the calling package exceeds the
+     * allowed rate limits and is throttled.
+     *
+     * This call does not inform the caller whether the custom audience specified existed in
+     * on-device storage. In other words, it will fail silently when a buyer attempts to leave a
+     * custom audience that was not joined.
+     *
+     * @param request The request to leave custom audience.
+     */
+    @RequiresPermission(AdServicesPermissions.ACCESS_ADSERVICES_CUSTOM_AUDIENCE)
+    abstract fun leaveCustomAudienceAsync(
+        request: LeaveCustomAudienceRequest
+    ): ListenableFuture<Unit>
+
+    private class Api33Ext4JavaImpl(
+        private val mCustomAudienceManager: CustomAudienceManager?
+    ) : CustomAudienceManagerFutures() {
+        @DoNotInline
+        @RequiresPermission(AdServicesPermissions.ACCESS_ADSERVICES_CUSTOM_AUDIENCE)
+        override fun joinCustomAudienceAsync(
+            request: JoinCustomAudienceRequest
+        ): ListenableFuture<Unit> {
+            return CoroutineScope(Dispatchers.Default).async {
+                mCustomAudienceManager!!.joinCustomAudience(request)
+            }.asListenableFuture()
+        }
+
+        @DoNotInline
+        @RequiresPermission(AdServicesPermissions.ACCESS_ADSERVICES_CUSTOM_AUDIENCE)
+        override fun leaveCustomAudienceAsync(
+            request: LeaveCustomAudienceRequest
+        ): ListenableFuture<Unit> {
+            return CoroutineScope(Dispatchers.Default).async {
+                mCustomAudienceManager!!.leaveCustomAudience(request)
+            }.asListenableFuture()
+        }
+    }
+
+    companion object {
+        /**
+         *  Creates [CustomAudienceManagerFutures].
+         *
+         *  @return CustomAudienceManagerFutures object. If the device is running an incompatible
+         *  build, the value returned is null.
+         */
+        @JvmStatic
+        fun from(context: Context): CustomAudienceManagerFutures? {
+            return obtain(context)?.let { Api33Ext4JavaImpl(it) }
+        }
+    }
+}
\ No newline at end of file
diff --git a/privacysandbox/ads/ads-adservices-java/src/main/java/androidx/privacysandbox/ads/adservices/java/internal/CoroutineAdapter.kt b/privacysandbox/ads/ads-adservices-java/src/main/java/androidx/privacysandbox/ads/adservices/java/internal/CoroutineAdapter.kt
new file mode 100644
index 0000000..b4f03c4
--- /dev/null
+++ b/privacysandbox/ads/ads-adservices-java/src/main/java/androidx/privacysandbox/ads/adservices/java/internal/CoroutineAdapter.kt
@@ -0,0 +1,44 @@
+/*
+ * 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.privacysandbox.ads.adservices.java.internal
+
+import androidx.concurrent.futures.CallbackToFutureAdapter
+import com.google.common.util.concurrent.ListenableFuture
+import java.util.concurrent.CancellationException
+import kotlinx.coroutines.Deferred
+import kotlinx.coroutines.ExperimentalCoroutinesApi
+
+@SuppressWarnings("AsyncSuffixFuture")
+@OptIn(ExperimentalCoroutinesApi::class)
+internal fun <T> Deferred<T>.asListenableFuture(
+    tag: Any? = "Deferred.asListenableFuture"
+): ListenableFuture<T> = CallbackToFutureAdapter.getFuture { completer ->
+
+    this.invokeOnCompletion {
+        if (it != null) {
+            if (it is CancellationException) {
+                completer.setCancelled()
+            } else {
+                completer.setException(it)
+            }
+        } else {
+            // Ignore exceptions - This should never throw in this situation.
+            completer.set(this.getCompleted())
+        }
+    }
+    tag
+}
diff --git a/privacysandbox/ads/ads-adservices-java/src/main/java/androidx/privacysandbox/ads/adservices/java/internal/package-info.java b/privacysandbox/ads/ads-adservices-java/src/main/java/androidx/privacysandbox/ads/adservices/java/internal/package-info.java
new file mode 100644
index 0000000..a3c8ac4
--- /dev/null
+++ b/privacysandbox/ads/ads-adservices-java/src/main/java/androidx/privacysandbox/ads/adservices/java/internal/package-info.java
@@ -0,0 +1,23 @@
+/*
+ * 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.
+ */
+
+/**
+ * @hide
+ */
+@RestrictTo(RestrictTo.Scope.LIBRARY)
+package androidx.privacysandbox.ads.adservices.java.internal;
+
+import androidx.annotation.RestrictTo;
diff --git a/privacysandbox/ads/ads-adservices-java/src/main/java/androidx/privacysandbox/ads/adservices/java/measurement/MeasurementManagerFutures.kt b/privacysandbox/ads/ads-adservices-java/src/main/java/androidx/privacysandbox/ads/adservices/java/measurement/MeasurementManagerFutures.kt
new file mode 100644
index 0000000..dd39ab0
--- /dev/null
+++ b/privacysandbox/ads/ads-adservices-java/src/main/java/androidx/privacysandbox/ads/adservices/java/measurement/MeasurementManagerFutures.kt
@@ -0,0 +1,186 @@
+/*
+ * 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.privacysandbox.ads.adservices.java.measurement
+
+import android.adservices.common.AdServicesPermissions
+import android.content.Context
+import android.net.Uri
+import android.view.InputEvent
+import androidx.annotation.DoNotInline
+import androidx.annotation.RequiresPermission
+import androidx.privacysandbox.ads.adservices.java.internal.asListenableFuture
+import androidx.privacysandbox.ads.adservices.measurement.DeletionRequest
+import androidx.privacysandbox.ads.adservices.measurement.MeasurementManager
+import androidx.privacysandbox.ads.adservices.measurement.MeasurementManager.Companion.obtain
+import androidx.privacysandbox.ads.adservices.measurement.WebSourceRegistrationRequest
+import androidx.privacysandbox.ads.adservices.measurement.WebTriggerRegistrationRequest
+import com.google.common.util.concurrent.ListenableFuture
+import kotlinx.coroutines.CoroutineScope
+import kotlinx.coroutines.Dispatchers
+import kotlinx.coroutines.async
+
+/**
+ * This provides APIs for App and Ad-Sdks to access Privacy Sandbox Measurement APIs in a privacy
+ * preserving way. This class can be used by Java clients.
+ */
+abstract class MeasurementManagerFutures internal constructor() {
+    /**
+     * Delete previous registrations.
+     *
+     * @param deletionRequest The request for deleting data.
+     * @return ListenableFuture. If the deletion is successful, result is null.
+     */
+    @SuppressWarnings("MissingNullability")
+    abstract fun deleteRegistrationsAsync(
+        deletionRequest: DeletionRequest
+    ): ListenableFuture<Unit>
+
+    /**
+     * Register an attribution source (click or view).
+     *
+     * @param attributionSource the platform issues a request to this URI in order to fetch metadata
+     *     associated with the attribution source.
+     * @param inputEvent either an [InputEvent] object (for a click event) or null (for a view
+     *     event).
+     */
+    @SuppressWarnings("MissingNullability")
+    @RequiresPermission(AdServicesPermissions.ACCESS_ADSERVICES_ATTRIBUTION)
+    abstract fun registerSourceAsync(
+        attributionSource: Uri,
+        inputEvent: InputEvent?
+    ): ListenableFuture<Unit>
+
+    /**
+     * Register a trigger (conversion).
+     *
+     * @param trigger the API issues a request to this URI to fetch metadata associated with the
+     *     trigger.
+     */
+    @SuppressWarnings("MissingNullability")
+    @RequiresPermission(AdServicesPermissions.ACCESS_ADSERVICES_ATTRIBUTION)
+    abstract fun registerTriggerAsync(trigger: Uri): ListenableFuture<Unit>
+
+    /**
+     * Register an attribution source(click or view) from web context. This API will not process any
+     * redirects, all registration URLs should be supplied with the request. At least one of
+     * appDestination or webDestination parameters are required to be provided.
+     *
+     * @param request source registration request
+     */
+    @SuppressWarnings("MissingNullability")
+    @RequiresPermission(AdServicesPermissions.ACCESS_ADSERVICES_ATTRIBUTION)
+    abstract fun registerWebSourceAsync(
+        request: WebSourceRegistrationRequest
+    ): ListenableFuture<Unit>
+
+    /**
+     * Register an attribution trigger(click or view) from web context. This API will not process
+     * any redirects, all registration URLs should be supplied with the request.
+     * OutcomeReceiver#onError}.
+     *
+     * @param request trigger registration request
+     */
+    @SuppressWarnings("MissingNullability")
+    @RequiresPermission(AdServicesPermissions.ACCESS_ADSERVICES_ATTRIBUTION)
+    abstract fun registerWebTriggerAsync(
+        request: WebTriggerRegistrationRequest,
+    ): ListenableFuture<Unit>
+
+    /**
+     * Get Measurement API status.
+     *
+     * The call returns an integer value (see [MeasurementManager.MEASUREMENT_API_STATE_DISABLED]
+     * and [MeasurementManager.MEASUREMENT_API_STATE_ENABLED] for possible values).
+     */
+    @SuppressWarnings("MissingNullability")
+    @RequiresPermission(AdServicesPermissions.ACCESS_ADSERVICES_ATTRIBUTION)
+    abstract fun getMeasurementApiStatusAsync(): ListenableFuture<Int>
+
+    private class Api33Ext4JavaImpl(
+        private val mMeasurementManager: MeasurementManager
+    ) : MeasurementManagerFutures() {
+        @DoNotInline
+        @RequiresPermission(AdServicesPermissions.ACCESS_ADSERVICES_ATTRIBUTION)
+        override fun deleteRegistrationsAsync(
+            deletionRequest: DeletionRequest
+        ): ListenableFuture<Unit> {
+            return CoroutineScope(Dispatchers.Main).async {
+                mMeasurementManager.deleteRegistrations(deletionRequest)
+            }.asListenableFuture()
+        }
+
+        @DoNotInline
+        @RequiresPermission(AdServicesPermissions.ACCESS_ADSERVICES_ATTRIBUTION)
+        override fun registerSourceAsync(
+            attributionSource: Uri,
+            inputEvent: InputEvent?
+        ): ListenableFuture<Unit> {
+            return CoroutineScope(Dispatchers.Main).async {
+                mMeasurementManager.registerSource(attributionSource, inputEvent)
+            }.asListenableFuture()
+        }
+
+        @DoNotInline
+        @RequiresPermission(AdServicesPermissions.ACCESS_ADSERVICES_ATTRIBUTION)
+        override fun registerTriggerAsync(trigger: Uri): ListenableFuture<Unit> {
+            return CoroutineScope(Dispatchers.Main).async {
+                mMeasurementManager.registerTrigger(trigger)
+            }.asListenableFuture()
+        }
+
+        @DoNotInline
+        @RequiresPermission(AdServicesPermissions.ACCESS_ADSERVICES_ATTRIBUTION)
+        override fun registerWebSourceAsync(
+            request: WebSourceRegistrationRequest
+        ): ListenableFuture<Unit> {
+            return CoroutineScope(Dispatchers.Main).async {
+                mMeasurementManager.registerWebSource(request)
+            }.asListenableFuture()
+        }
+
+        @DoNotInline
+        @RequiresPermission(AdServicesPermissions.ACCESS_ADSERVICES_ATTRIBUTION)
+        override fun registerWebTriggerAsync(
+            request: WebTriggerRegistrationRequest,
+        ): ListenableFuture<Unit> {
+            return CoroutineScope(Dispatchers.Main).async {
+                mMeasurementManager.registerWebTrigger(request)
+            }.asListenableFuture()
+        }
+
+        @DoNotInline
+        @RequiresPermission(AdServicesPermissions.ACCESS_ADSERVICES_ATTRIBUTION)
+        override fun getMeasurementApiStatusAsync(): ListenableFuture<Int> {
+            return CoroutineScope(Dispatchers.Main).async {
+                mMeasurementManager.getMeasurementApiStatus()
+            }.asListenableFuture()
+        }
+    }
+
+    companion object {
+        /**
+         *  Creates [MeasurementManagerFutures].
+         *
+         *  @return MeasurementManagerFutures object. If the device is running an incompatible
+         *  build, the value returned is null.
+         */
+        @JvmStatic
+        fun from(context: Context): MeasurementManagerFutures? {
+            return obtain(context)?.let { Api33Ext4JavaImpl(it) }
+        }
+    }
+}
\ No newline at end of file
diff --git a/privacysandbox/ads/ads-adservices-java/src/main/java/androidx/privacysandbox/ads/adservices/java/topics/TopicsManagerFutures.kt b/privacysandbox/ads/ads-adservices-java/src/main/java/androidx/privacysandbox/ads/adservices/java/topics/TopicsManagerFutures.kt
new file mode 100644
index 0000000..5f80cd7
--- /dev/null
+++ b/privacysandbox/ads/ads-adservices-java/src/main/java/androidx/privacysandbox/ads/adservices/java/topics/TopicsManagerFutures.kt
@@ -0,0 +1,73 @@
+/*
+ * 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.privacysandbox.ads.adservices.java.topics
+
+import androidx.privacysandbox.ads.adservices.java.internal.asListenableFuture
+import androidx.privacysandbox.ads.adservices.topics.TopicsManager
+import androidx.privacysandbox.ads.adservices.topics.TopicsManager.Companion.obtain
+import androidx.privacysandbox.ads.adservices.topics.GetTopicsRequest
+import androidx.privacysandbox.ads.adservices.topics.GetTopicsResponse
+import android.adservices.common.AdServicesPermissions
+import android.content.Context
+import androidx.annotation.DoNotInline
+import androidx.annotation.RequiresPermission
+import com.google.common.util.concurrent.ListenableFuture
+import kotlinx.coroutines.CoroutineScope
+import kotlinx.coroutines.Dispatchers
+import kotlinx.coroutines.async
+
+/**
+ * This provides APIs for App and Ad-Sdks to get the user interest topics in a privacy
+ * preserving way. This class can be used by Java clients.
+ */
+abstract class TopicsManagerFutures internal constructor() {
+    /**
+     * Returns the topics.
+     *
+     * @param request The GetTopicsRequest for obtaining Topics.
+     * @return ListenableFuture to get the Topics response.
+     */
+    @RequiresPermission(AdServicesPermissions.ACCESS_ADSERVICES_TOPICS)
+    abstract fun getTopicsAsync(request: GetTopicsRequest): ListenableFuture<GetTopicsResponse>
+
+    private class Api33Ext4JavaImpl(
+        private val mTopicsManager: TopicsManager
+    ) : TopicsManagerFutures() {
+        @DoNotInline
+        @RequiresPermission(AdServicesPermissions.ACCESS_ADSERVICES_TOPICS)
+        override fun getTopicsAsync(
+            request: GetTopicsRequest
+        ): ListenableFuture<GetTopicsResponse> {
+            return CoroutineScope(Dispatchers.Main).async {
+                mTopicsManager.getTopics(request)
+            }.asListenableFuture()
+        }
+    }
+
+    companion object {
+        /**
+         *  Creates [TopicsManagerFutures].
+         *
+         *  @return TopicsManagerFutures object. If the device is running an incompatible
+         *  build, the value returned is null.
+         */
+        @JvmStatic
+        fun from(context: Context): TopicsManagerFutures? {
+            return obtain(context)?.let { Api33Ext4JavaImpl(it) }
+        }
+    }
+}
\ No newline at end of file
diff --git a/privacysandbox/ads/ads-adservices/api/current.txt b/privacysandbox/ads/ads-adservices/api/current.txt
new file mode 100644
index 0000000..01aa5c6
--- /dev/null
+++ b/privacysandbox/ads/ads-adservices/api/current.txt
@@ -0,0 +1,345 @@
+// Signature format: 4.0
+package androidx.privacysandbox.ads.adservices.adid {
+
+  public final class AdId {
+    method public String getAdId();
+    method public boolean isLimitAdTrackingEnabled();
+    property public final String adId;
+    property public final boolean isLimitAdTrackingEnabled;
+  }
+
+  public abstract class AdIdManager {
+    method @RequiresPermission(android.adservices.common.AdServicesPermissions.ACCESS_ADSERVICES_AD_ID) public abstract suspend Object? getAdId(kotlin.coroutines.Continuation<? super androidx.privacysandbox.ads.adservices.adid.AdId>);
+    method public static final androidx.privacysandbox.ads.adservices.adid.AdIdManager? obtain(android.content.Context context);
+    field public static final androidx.privacysandbox.ads.adservices.adid.AdIdManager.Companion Companion;
+  }
+
+  public static final class AdIdManager.Companion {
+    method public androidx.privacysandbox.ads.adservices.adid.AdIdManager? obtain(android.content.Context context);
+  }
+
+}
+
+package androidx.privacysandbox.ads.adservices.adselection {
+
+  public final class AdSelectionConfig {
+    ctor public AdSelectionConfig(androidx.privacysandbox.ads.adservices.common.AdTechIdentifier seller, android.net.Uri decisionLogicUri, java.util.List<androidx.privacysandbox.ads.adservices.common.AdTechIdentifier> customAudienceBuyers, androidx.privacysandbox.ads.adservices.common.AdSelectionSignals adSelectionSignals, androidx.privacysandbox.ads.adservices.common.AdSelectionSignals sellerSignals, java.util.Map<androidx.privacysandbox.ads.adservices.common.AdTechIdentifier,androidx.privacysandbox.ads.adservices.common.AdSelectionSignals> perBuyerSignals, android.net.Uri trustedScoringSignalsUri);
+    method public androidx.privacysandbox.ads.adservices.common.AdSelectionSignals getAdSelectionSignals();
+    method public java.util.List<androidx.privacysandbox.ads.adservices.common.AdTechIdentifier> getCustomAudienceBuyers();
+    method public android.net.Uri getDecisionLogicUri();
+    method public java.util.Map<androidx.privacysandbox.ads.adservices.common.AdTechIdentifier,androidx.privacysandbox.ads.adservices.common.AdSelectionSignals> getPerBuyerSignals();
+    method public androidx.privacysandbox.ads.adservices.common.AdTechIdentifier getSeller();
+    method public androidx.privacysandbox.ads.adservices.common.AdSelectionSignals getSellerSignals();
+    method public android.net.Uri getTrustedScoringSignalsUri();
+    property public final androidx.privacysandbox.ads.adservices.common.AdSelectionSignals adSelectionSignals;
+    property public final java.util.List<androidx.privacysandbox.ads.adservices.common.AdTechIdentifier> customAudienceBuyers;
+    property public final android.net.Uri decisionLogicUri;
+    property public final java.util.Map<androidx.privacysandbox.ads.adservices.common.AdTechIdentifier,androidx.privacysandbox.ads.adservices.common.AdSelectionSignals> perBuyerSignals;
+    property public final androidx.privacysandbox.ads.adservices.common.AdTechIdentifier seller;
+    property public final androidx.privacysandbox.ads.adservices.common.AdSelectionSignals sellerSignals;
+    property public final android.net.Uri trustedScoringSignalsUri;
+  }
+
+  public abstract class AdSelectionManager {
+    method public static final androidx.privacysandbox.ads.adservices.adselection.AdSelectionManager? obtain(android.content.Context context);
+    method @RequiresPermission(android.adservices.common.AdServicesPermissions.ACCESS_ADSERVICES_CUSTOM_AUDIENCE) public abstract suspend Object? reportImpression(androidx.privacysandbox.ads.adservices.adselection.ReportImpressionRequest reportImpressionRequest, kotlin.coroutines.Continuation<? super kotlin.Unit>);
+    method @RequiresPermission(android.adservices.common.AdServicesPermissions.ACCESS_ADSERVICES_CUSTOM_AUDIENCE) public abstract suspend Object? selectAds(androidx.privacysandbox.ads.adservices.adselection.AdSelectionConfig adSelectionConfig, kotlin.coroutines.Continuation<? super androidx.privacysandbox.ads.adservices.adselection.AdSelectionOutcome>);
+    field public static final androidx.privacysandbox.ads.adservices.adselection.AdSelectionManager.Companion Companion;
+  }
+
+  public static final class AdSelectionManager.Companion {
+    method public androidx.privacysandbox.ads.adservices.adselection.AdSelectionManager? obtain(android.content.Context context);
+  }
+
+  public final class AdSelectionOutcome {
+    ctor public AdSelectionOutcome(long adSelectionId, android.net.Uri renderUri);
+    method public long getAdSelectionId();
+    method public android.net.Uri getRenderUri();
+    property public final long adSelectionId;
+    property public final android.net.Uri renderUri;
+  }
+
+  public final class ReportImpressionRequest {
+    ctor public ReportImpressionRequest(long adSelectionId, androidx.privacysandbox.ads.adservices.adselection.AdSelectionConfig adSelectionConfig);
+    method public androidx.privacysandbox.ads.adservices.adselection.AdSelectionConfig getAdSelectionConfig();
+    method public long getAdSelectionId();
+    property public final androidx.privacysandbox.ads.adservices.adselection.AdSelectionConfig adSelectionConfig;
+    property public final long adSelectionId;
+  }
+
+}
+
+package androidx.privacysandbox.ads.adservices.appsetid {
+
+  public final class AppSetId {
+    ctor public AppSetId(String id, int scope);
+    method public String getId();
+    method public int getScope();
+    property public final String id;
+    property public final int scope;
+    field public static final androidx.privacysandbox.ads.adservices.appsetid.AppSetId.Companion Companion;
+    field public static final int SCOPE_APP = 1; // 0x1
+    field public static final int SCOPE_DEVELOPER = 2; // 0x2
+  }
+
+  public static final class AppSetId.Companion {
+  }
+
+  public abstract class AppSetIdManager {
+    method public abstract suspend Object? getAppSetId(kotlin.coroutines.Continuation<? super androidx.privacysandbox.ads.adservices.appsetid.AppSetId>);
+    method public static final androidx.privacysandbox.ads.adservices.appsetid.AppSetIdManager? obtain(android.content.Context context);
+    field public static final androidx.privacysandbox.ads.adservices.appsetid.AppSetIdManager.Companion Companion;
+  }
+
+  public static final class AppSetIdManager.Companion {
+    method public androidx.privacysandbox.ads.adservices.appsetid.AppSetIdManager? obtain(android.content.Context context);
+  }
+
+}
+
+package androidx.privacysandbox.ads.adservices.common {
+
+  public final class AdData {
+    ctor public AdData(android.net.Uri renderUri, String metadata);
+    method public String getMetadata();
+    method public android.net.Uri getRenderUri();
+    property public final String metadata;
+    property public final android.net.Uri renderUri;
+  }
+
+  public final class AdSelectionSignals {
+    ctor public AdSelectionSignals(String signals);
+    method public String getSignals();
+    property public final String signals;
+  }
+
+  public final class AdTechIdentifier {
+    ctor public AdTechIdentifier(String identifier);
+    method public String getIdentifier();
+    property public final String identifier;
+  }
+
+}
+
+package androidx.privacysandbox.ads.adservices.customaudience {
+
+  public final class CustomAudience {
+    ctor public CustomAudience(androidx.privacysandbox.ads.adservices.common.AdTechIdentifier buyer, String name, android.net.Uri dailyUpdateUri, android.net.Uri biddingLogicUri, java.util.List<androidx.privacysandbox.ads.adservices.common.AdData> ads, optional java.time.Instant? activationTime, optional java.time.Instant? expirationTime, optional androidx.privacysandbox.ads.adservices.common.AdSelectionSignals? userBiddingSignals, optional androidx.privacysandbox.ads.adservices.customaudience.TrustedBiddingData? trustedBiddingSignals);
+    method public java.time.Instant? getActivationTime();
+    method public java.util.List<androidx.privacysandbox.ads.adservices.common.AdData> getAds();
+    method public android.net.Uri getBiddingLogicUri();
+    method public androidx.privacysandbox.ads.adservices.common.AdTechIdentifier getBuyer();
+    method public android.net.Uri getDailyUpdateUri();
+    method public java.time.Instant? getExpirationTime();
+    method public String getName();
+    method public androidx.privacysandbox.ads.adservices.customaudience.TrustedBiddingData? getTrustedBiddingSignals();
+    method public androidx.privacysandbox.ads.adservices.common.AdSelectionSignals? getUserBiddingSignals();
+    property public final java.time.Instant? activationTime;
+    property public final java.util.List<androidx.privacysandbox.ads.adservices.common.AdData> ads;
+    property public final android.net.Uri biddingLogicUri;
+    property public final androidx.privacysandbox.ads.adservices.common.AdTechIdentifier buyer;
+    property public final android.net.Uri dailyUpdateUri;
+    property public final java.time.Instant? expirationTime;
+    property public final String name;
+    property public final androidx.privacysandbox.ads.adservices.customaudience.TrustedBiddingData? trustedBiddingSignals;
+    property public final androidx.privacysandbox.ads.adservices.common.AdSelectionSignals? userBiddingSignals;
+  }
+
+  public static final class CustomAudience.Builder {
+    ctor public CustomAudience.Builder(androidx.privacysandbox.ads.adservices.common.AdTechIdentifier buyer, String name, android.net.Uri dailyUpdateUri, android.net.Uri biddingLogicUri, java.util.List<androidx.privacysandbox.ads.adservices.common.AdData> ads);
+    method public androidx.privacysandbox.ads.adservices.customaudience.CustomAudience build();
+    method public androidx.privacysandbox.ads.adservices.customaudience.CustomAudience.Builder setActivationTime(java.time.Instant activationTime);
+    method public androidx.privacysandbox.ads.adservices.customaudience.CustomAudience.Builder setAds(java.util.List<androidx.privacysandbox.ads.adservices.common.AdData> ads);
+    method public androidx.privacysandbox.ads.adservices.customaudience.CustomAudience.Builder setBiddingLogicUri(android.net.Uri biddingLogicUri);
+    method public androidx.privacysandbox.ads.adservices.customaudience.CustomAudience.Builder setBuyer(androidx.privacysandbox.ads.adservices.common.AdTechIdentifier buyer);
+    method public androidx.privacysandbox.ads.adservices.customaudience.CustomAudience.Builder setDailyUpdateUri(android.net.Uri dailyUpdateUri);
+    method public androidx.privacysandbox.ads.adservices.customaudience.CustomAudience.Builder setExpirationTime(java.time.Instant expirationTime);
+    method public androidx.privacysandbox.ads.adservices.customaudience.CustomAudience.Builder setName(String name);
+    method public androidx.privacysandbox.ads.adservices.customaudience.CustomAudience.Builder setTrustedBiddingData(androidx.privacysandbox.ads.adservices.customaudience.TrustedBiddingData trustedBiddingSignals);
+    method public androidx.privacysandbox.ads.adservices.customaudience.CustomAudience.Builder setUserBiddingSignals(androidx.privacysandbox.ads.adservices.common.AdSelectionSignals userBiddingSignals);
+  }
+
+  public abstract class CustomAudienceManager {
+    method @RequiresPermission(android.adservices.common.AdServicesPermissions.ACCESS_ADSERVICES_CUSTOM_AUDIENCE) public abstract suspend Object? joinCustomAudience(androidx.privacysandbox.ads.adservices.customaudience.JoinCustomAudienceRequest request, kotlin.coroutines.Continuation<? super kotlin.Unit>);
+    method @RequiresPermission(android.adservices.common.AdServicesPermissions.ACCESS_ADSERVICES_CUSTOM_AUDIENCE) public abstract suspend Object? leaveCustomAudience(androidx.privacysandbox.ads.adservices.customaudience.LeaveCustomAudienceRequest request, kotlin.coroutines.Continuation<? super kotlin.Unit>);
+    method public static final androidx.privacysandbox.ads.adservices.customaudience.CustomAudienceManager? obtain(android.content.Context context);
+    field public static final androidx.privacysandbox.ads.adservices.customaudience.CustomAudienceManager.Companion Companion;
+  }
+
+  public static final class CustomAudienceManager.Companion {
+    method public androidx.privacysandbox.ads.adservices.customaudience.CustomAudienceManager? obtain(android.content.Context context);
+  }
+
+  public final class JoinCustomAudienceRequest {
+    ctor public JoinCustomAudienceRequest(androidx.privacysandbox.ads.adservices.customaudience.CustomAudience customAudience);
+    method public androidx.privacysandbox.ads.adservices.customaudience.CustomAudience getCustomAudience();
+    property public final androidx.privacysandbox.ads.adservices.customaudience.CustomAudience customAudience;
+  }
+
+  public final class LeaveCustomAudienceRequest {
+    ctor public LeaveCustomAudienceRequest(androidx.privacysandbox.ads.adservices.common.AdTechIdentifier buyer, String name);
+    method public androidx.privacysandbox.ads.adservices.common.AdTechIdentifier getBuyer();
+    method public String getName();
+    property public final androidx.privacysandbox.ads.adservices.common.AdTechIdentifier buyer;
+    property public final String name;
+  }
+
+  public final class TrustedBiddingData {
+    ctor public TrustedBiddingData(android.net.Uri trustedBiddingUri, java.util.List<java.lang.String> trustedBiddingKeys);
+    method public java.util.List<java.lang.String> getTrustedBiddingKeys();
+    method public android.net.Uri getTrustedBiddingUri();
+    property public final java.util.List<java.lang.String> trustedBiddingKeys;
+    property public final android.net.Uri trustedBiddingUri;
+  }
+
+}
+
+package androidx.privacysandbox.ads.adservices.measurement {
+
+  @RequiresApi(android.os.Build.VERSION_CODES.TIRAMISU) public final class DeletionRequest {
+    ctor public DeletionRequest(int deletionMode, int matchBehavior, optional java.time.Instant start, optional java.time.Instant end, optional java.util.List<? extends android.net.Uri> domainUris, optional java.util.List<? extends android.net.Uri> originUris);
+    method public int getDeletionMode();
+    method public java.util.List<android.net.Uri> getDomainUris();
+    method public java.time.Instant getEnd();
+    method public int getMatchBehavior();
+    method public java.util.List<android.net.Uri> getOriginUris();
+    method public java.time.Instant getStart();
+    property public final int deletionMode;
+    property public final java.util.List<android.net.Uri> domainUris;
+    property public final java.time.Instant end;
+    property public final int matchBehavior;
+    property public final java.util.List<android.net.Uri> originUris;
+    property public final java.time.Instant start;
+    field public static final androidx.privacysandbox.ads.adservices.measurement.DeletionRequest.Companion Companion;
+    field public static final int DELETION_MODE_ALL = 0; // 0x0
+    field public static final int DELETION_MODE_EXCLUDE_INTERNAL_DATA = 1; // 0x1
+    field public static final int MATCH_BEHAVIOR_DELETE = 0; // 0x0
+    field public static final int MATCH_BEHAVIOR_PRESERVE = 1; // 0x1
+  }
+
+  @RequiresApi(android.os.Build.VERSION_CODES.TIRAMISU) public static final class DeletionRequest.Builder {
+    ctor public DeletionRequest.Builder(int deletionMode, int matchBehavior);
+    method public androidx.privacysandbox.ads.adservices.measurement.DeletionRequest build();
+    method public androidx.privacysandbox.ads.adservices.measurement.DeletionRequest.Builder setDomainUris(java.util.List<? extends android.net.Uri> domainUris);
+    method public androidx.privacysandbox.ads.adservices.measurement.DeletionRequest.Builder setEnd(java.time.Instant end);
+    method public androidx.privacysandbox.ads.adservices.measurement.DeletionRequest.Builder setOriginUris(java.util.List<? extends android.net.Uri> originUris);
+    method public androidx.privacysandbox.ads.adservices.measurement.DeletionRequest.Builder setStart(java.time.Instant start);
+  }
+
+  public static final class DeletionRequest.Companion {
+  }
+
+  public abstract class MeasurementManager {
+    ctor public MeasurementManager();
+    method public abstract suspend Object? deleteRegistrations(androidx.privacysandbox.ads.adservices.measurement.DeletionRequest deletionRequest, kotlin.coroutines.Continuation<? super kotlin.Unit>);
+    method @RequiresPermission(android.adservices.common.AdServicesPermissions.ACCESS_ADSERVICES_ATTRIBUTION) public abstract suspend Object? getMeasurementApiStatus(kotlin.coroutines.Continuation<? super java.lang.Integer>);
+    method public static final androidx.privacysandbox.ads.adservices.measurement.MeasurementManager? obtain(android.content.Context context);
+    method @RequiresPermission(android.adservices.common.AdServicesPermissions.ACCESS_ADSERVICES_ATTRIBUTION) public abstract suspend Object? registerSource(android.net.Uri attributionSource, android.view.InputEvent? inputEvent, kotlin.coroutines.Continuation<? super kotlin.Unit>);
+    method @RequiresPermission(android.adservices.common.AdServicesPermissions.ACCESS_ADSERVICES_ATTRIBUTION) public abstract suspend Object? registerTrigger(android.net.Uri trigger, kotlin.coroutines.Continuation<? super kotlin.Unit>);
+    method @RequiresPermission(android.adservices.common.AdServicesPermissions.ACCESS_ADSERVICES_ATTRIBUTION) public abstract suspend Object? registerWebSource(androidx.privacysandbox.ads.adservices.measurement.WebSourceRegistrationRequest request, kotlin.coroutines.Continuation<? super kotlin.Unit>);
+    method @RequiresPermission(android.adservices.common.AdServicesPermissions.ACCESS_ADSERVICES_ATTRIBUTION) public abstract suspend Object? registerWebTrigger(androidx.privacysandbox.ads.adservices.measurement.WebTriggerRegistrationRequest request, kotlin.coroutines.Continuation<? super kotlin.Unit>);
+    field public static final androidx.privacysandbox.ads.adservices.measurement.MeasurementManager.Companion Companion;
+    field public static final int MEASUREMENT_API_STATE_DISABLED = 0; // 0x0
+    field public static final int MEASUREMENT_API_STATE_ENABLED = 1; // 0x1
+  }
+
+  public static final class MeasurementManager.Companion {
+    method public androidx.privacysandbox.ads.adservices.measurement.MeasurementManager? obtain(android.content.Context context);
+  }
+
+  @RequiresApi(android.os.Build.VERSION_CODES.TIRAMISU) public final class WebSourceParams {
+    ctor public WebSourceParams(android.net.Uri registrationUri, boolean debugKeyAllowed);
+    method public boolean getDebugKeyAllowed();
+    method public android.net.Uri getRegistrationUri();
+    property public final boolean debugKeyAllowed;
+    property public final android.net.Uri registrationUri;
+  }
+
+  @RequiresApi(android.os.Build.VERSION_CODES.TIRAMISU) public final class WebSourceRegistrationRequest {
+    ctor public WebSourceRegistrationRequest(java.util.List<androidx.privacysandbox.ads.adservices.measurement.WebSourceParams> webSourceParams, android.net.Uri topOriginUri, optional android.view.InputEvent? inputEvent, optional android.net.Uri? appDestination, optional android.net.Uri? webDestination, optional android.net.Uri? verifiedDestination);
+    method public android.net.Uri? getAppDestination();
+    method public android.view.InputEvent? getInputEvent();
+    method public android.net.Uri getTopOriginUri();
+    method public android.net.Uri? getVerifiedDestination();
+    method public android.net.Uri? getWebDestination();
+    method public java.util.List<androidx.privacysandbox.ads.adservices.measurement.WebSourceParams> getWebSourceParams();
+    property public final android.net.Uri? appDestination;
+    property public final android.view.InputEvent? inputEvent;
+    property public final android.net.Uri topOriginUri;
+    property public final android.net.Uri? verifiedDestination;
+    property public final android.net.Uri? webDestination;
+    property public final java.util.List<androidx.privacysandbox.ads.adservices.measurement.WebSourceParams> webSourceParams;
+  }
+
+  public static final class WebSourceRegistrationRequest.Builder {
+    ctor public WebSourceRegistrationRequest.Builder(java.util.List<androidx.privacysandbox.ads.adservices.measurement.WebSourceParams> webSourceParams, android.net.Uri topOriginUri);
+    method public androidx.privacysandbox.ads.adservices.measurement.WebSourceRegistrationRequest build();
+    method public androidx.privacysandbox.ads.adservices.measurement.WebSourceRegistrationRequest.Builder setAppDestination(android.net.Uri? appDestination);
+    method public androidx.privacysandbox.ads.adservices.measurement.WebSourceRegistrationRequest.Builder setInputEvent(android.view.InputEvent inputEvent);
+    method public androidx.privacysandbox.ads.adservices.measurement.WebSourceRegistrationRequest.Builder setVerifiedDestination(android.net.Uri? verifiedDestination);
+    method public androidx.privacysandbox.ads.adservices.measurement.WebSourceRegistrationRequest.Builder setWebDestination(android.net.Uri? webDestination);
+  }
+
+  @RequiresApi(android.os.Build.VERSION_CODES.TIRAMISU) public final class WebTriggerParams {
+    ctor public WebTriggerParams(android.net.Uri registrationUri, boolean debugKeyAllowed);
+    method public boolean getDebugKeyAllowed();
+    method public android.net.Uri getRegistrationUri();
+    property public final boolean debugKeyAllowed;
+    property public final android.net.Uri registrationUri;
+  }
+
+  @RequiresApi(android.os.Build.VERSION_CODES.TIRAMISU) public final class WebTriggerRegistrationRequest {
+    ctor public WebTriggerRegistrationRequest(java.util.List<androidx.privacysandbox.ads.adservices.measurement.WebTriggerParams> webTriggerParams, android.net.Uri destination);
+    method public android.net.Uri getDestination();
+    method public java.util.List<androidx.privacysandbox.ads.adservices.measurement.WebTriggerParams> getWebTriggerParams();
+    property public final android.net.Uri destination;
+    property public final java.util.List<androidx.privacysandbox.ads.adservices.measurement.WebTriggerParams> webTriggerParams;
+  }
+
+}
+
+package androidx.privacysandbox.ads.adservices.topics {
+
+  public final class GetTopicsRequest {
+    ctor public GetTopicsRequest(optional String sdkName, optional boolean shouldRecordObservation);
+    method public String getSdkName();
+    method public boolean getShouldRecordObservation();
+    property public final String sdkName;
+    property public final boolean shouldRecordObservation;
+  }
+
+  public static final class GetTopicsRequest.Builder {
+    ctor public GetTopicsRequest.Builder();
+    method public androidx.privacysandbox.ads.adservices.topics.GetTopicsRequest build();
+    method public androidx.privacysandbox.ads.adservices.topics.GetTopicsRequest.Builder setSdkName(String sdkName);
+    method public androidx.privacysandbox.ads.adservices.topics.GetTopicsRequest.Builder setShouldRecordObservation(boolean shouldRecordObservation);
+  }
+
+  public final class GetTopicsResponse {
+    ctor public GetTopicsResponse(java.util.List<androidx.privacysandbox.ads.adservices.topics.Topic> topics);
+    method public java.util.List<androidx.privacysandbox.ads.adservices.topics.Topic> getTopics();
+    property public final java.util.List<androidx.privacysandbox.ads.adservices.topics.Topic> topics;
+  }
+
+  public final class Topic {
+    ctor public Topic(long taxonomyVersion, long modelVersion, int topicId);
+    method public long getModelVersion();
+    method public long getTaxonomyVersion();
+    method public int getTopicId();
+    property public final long modelVersion;
+    property public final long taxonomyVersion;
+    property public final int topicId;
+  }
+
+  public abstract class TopicsManager {
+    method @RequiresPermission(android.adservices.common.AdServicesPermissions.ACCESS_ADSERVICES_TOPICS) public abstract suspend Object? getTopics(androidx.privacysandbox.ads.adservices.topics.GetTopicsRequest request, kotlin.coroutines.Continuation<? super androidx.privacysandbox.ads.adservices.topics.GetTopicsResponse>);
+    method public static final androidx.privacysandbox.ads.adservices.topics.TopicsManager? obtain(android.content.Context context);
+    field public static final androidx.privacysandbox.ads.adservices.topics.TopicsManager.Companion Companion;
+  }
+
+  public static final class TopicsManager.Companion {
+    method public androidx.privacysandbox.ads.adservices.topics.TopicsManager? obtain(android.content.Context context);
+  }
+
+}
+
diff --git a/privacysandbox/ads/ads-adservices/api/public_plus_experimental_current.txt b/privacysandbox/ads/ads-adservices/api/public_plus_experimental_current.txt
new file mode 100644
index 0000000..01aa5c6
--- /dev/null
+++ b/privacysandbox/ads/ads-adservices/api/public_plus_experimental_current.txt
@@ -0,0 +1,345 @@
+// Signature format: 4.0
+package androidx.privacysandbox.ads.adservices.adid {
+
+  public final class AdId {
+    method public String getAdId();
+    method public boolean isLimitAdTrackingEnabled();
+    property public final String adId;
+    property public final boolean isLimitAdTrackingEnabled;
+  }
+
+  public abstract class AdIdManager {
+    method @RequiresPermission(android.adservices.common.AdServicesPermissions.ACCESS_ADSERVICES_AD_ID) public abstract suspend Object? getAdId(kotlin.coroutines.Continuation<? super androidx.privacysandbox.ads.adservices.adid.AdId>);
+    method public static final androidx.privacysandbox.ads.adservices.adid.AdIdManager? obtain(android.content.Context context);
+    field public static final androidx.privacysandbox.ads.adservices.adid.AdIdManager.Companion Companion;
+  }
+
+  public static final class AdIdManager.Companion {
+    method public androidx.privacysandbox.ads.adservices.adid.AdIdManager? obtain(android.content.Context context);
+  }
+
+}
+
+package androidx.privacysandbox.ads.adservices.adselection {
+
+  public final class AdSelectionConfig {
+    ctor public AdSelectionConfig(androidx.privacysandbox.ads.adservices.common.AdTechIdentifier seller, android.net.Uri decisionLogicUri, java.util.List<androidx.privacysandbox.ads.adservices.common.AdTechIdentifier> customAudienceBuyers, androidx.privacysandbox.ads.adservices.common.AdSelectionSignals adSelectionSignals, androidx.privacysandbox.ads.adservices.common.AdSelectionSignals sellerSignals, java.util.Map<androidx.privacysandbox.ads.adservices.common.AdTechIdentifier,androidx.privacysandbox.ads.adservices.common.AdSelectionSignals> perBuyerSignals, android.net.Uri trustedScoringSignalsUri);
+    method public androidx.privacysandbox.ads.adservices.common.AdSelectionSignals getAdSelectionSignals();
+    method public java.util.List<androidx.privacysandbox.ads.adservices.common.AdTechIdentifier> getCustomAudienceBuyers();
+    method public android.net.Uri getDecisionLogicUri();
+    method public java.util.Map<androidx.privacysandbox.ads.adservices.common.AdTechIdentifier,androidx.privacysandbox.ads.adservices.common.AdSelectionSignals> getPerBuyerSignals();
+    method public androidx.privacysandbox.ads.adservices.common.AdTechIdentifier getSeller();
+    method public androidx.privacysandbox.ads.adservices.common.AdSelectionSignals getSellerSignals();
+    method public android.net.Uri getTrustedScoringSignalsUri();
+    property public final androidx.privacysandbox.ads.adservices.common.AdSelectionSignals adSelectionSignals;
+    property public final java.util.List<androidx.privacysandbox.ads.adservices.common.AdTechIdentifier> customAudienceBuyers;
+    property public final android.net.Uri decisionLogicUri;
+    property public final java.util.Map<androidx.privacysandbox.ads.adservices.common.AdTechIdentifier,androidx.privacysandbox.ads.adservices.common.AdSelectionSignals> perBuyerSignals;
+    property public final androidx.privacysandbox.ads.adservices.common.AdTechIdentifier seller;
+    property public final androidx.privacysandbox.ads.adservices.common.AdSelectionSignals sellerSignals;
+    property public final android.net.Uri trustedScoringSignalsUri;
+  }
+
+  public abstract class AdSelectionManager {
+    method public static final androidx.privacysandbox.ads.adservices.adselection.AdSelectionManager? obtain(android.content.Context context);
+    method @RequiresPermission(android.adservices.common.AdServicesPermissions.ACCESS_ADSERVICES_CUSTOM_AUDIENCE) public abstract suspend Object? reportImpression(androidx.privacysandbox.ads.adservices.adselection.ReportImpressionRequest reportImpressionRequest, kotlin.coroutines.Continuation<? super kotlin.Unit>);
+    method @RequiresPermission(android.adservices.common.AdServicesPermissions.ACCESS_ADSERVICES_CUSTOM_AUDIENCE) public abstract suspend Object? selectAds(androidx.privacysandbox.ads.adservices.adselection.AdSelectionConfig adSelectionConfig, kotlin.coroutines.Continuation<? super androidx.privacysandbox.ads.adservices.adselection.AdSelectionOutcome>);
+    field public static final androidx.privacysandbox.ads.adservices.adselection.AdSelectionManager.Companion Companion;
+  }
+
+  public static final class AdSelectionManager.Companion {
+    method public androidx.privacysandbox.ads.adservices.adselection.AdSelectionManager? obtain(android.content.Context context);
+  }
+
+  public final class AdSelectionOutcome {
+    ctor public AdSelectionOutcome(long adSelectionId, android.net.Uri renderUri);
+    method public long getAdSelectionId();
+    method public android.net.Uri getRenderUri();
+    property public final long adSelectionId;
+    property public final android.net.Uri renderUri;
+  }
+
+  public final class ReportImpressionRequest {
+    ctor public ReportImpressionRequest(long adSelectionId, androidx.privacysandbox.ads.adservices.adselection.AdSelectionConfig adSelectionConfig);
+    method public androidx.privacysandbox.ads.adservices.adselection.AdSelectionConfig getAdSelectionConfig();
+    method public long getAdSelectionId();
+    property public final androidx.privacysandbox.ads.adservices.adselection.AdSelectionConfig adSelectionConfig;
+    property public final long adSelectionId;
+  }
+
+}
+
+package androidx.privacysandbox.ads.adservices.appsetid {
+
+  public final class AppSetId {
+    ctor public AppSetId(String id, int scope);
+    method public String getId();
+    method public int getScope();
+    property public final String id;
+    property public final int scope;
+    field public static final androidx.privacysandbox.ads.adservices.appsetid.AppSetId.Companion Companion;
+    field public static final int SCOPE_APP = 1; // 0x1
+    field public static final int SCOPE_DEVELOPER = 2; // 0x2
+  }
+
+  public static final class AppSetId.Companion {
+  }
+
+  public abstract class AppSetIdManager {
+    method public abstract suspend Object? getAppSetId(kotlin.coroutines.Continuation<? super androidx.privacysandbox.ads.adservices.appsetid.AppSetId>);
+    method public static final androidx.privacysandbox.ads.adservices.appsetid.AppSetIdManager? obtain(android.content.Context context);
+    field public static final androidx.privacysandbox.ads.adservices.appsetid.AppSetIdManager.Companion Companion;
+  }
+
+  public static final class AppSetIdManager.Companion {
+    method public androidx.privacysandbox.ads.adservices.appsetid.AppSetIdManager? obtain(android.content.Context context);
+  }
+
+}
+
+package androidx.privacysandbox.ads.adservices.common {
+
+  public final class AdData {
+    ctor public AdData(android.net.Uri renderUri, String metadata);
+    method public String getMetadata();
+    method public android.net.Uri getRenderUri();
+    property public final String metadata;
+    property public final android.net.Uri renderUri;
+  }
+
+  public final class AdSelectionSignals {
+    ctor public AdSelectionSignals(String signals);
+    method public String getSignals();
+    property public final String signals;
+  }
+
+  public final class AdTechIdentifier {
+    ctor public AdTechIdentifier(String identifier);
+    method public String getIdentifier();
+    property public final String identifier;
+  }
+
+}
+
+package androidx.privacysandbox.ads.adservices.customaudience {
+
+  public final class CustomAudience {
+    ctor public CustomAudience(androidx.privacysandbox.ads.adservices.common.AdTechIdentifier buyer, String name, android.net.Uri dailyUpdateUri, android.net.Uri biddingLogicUri, java.util.List<androidx.privacysandbox.ads.adservices.common.AdData> ads, optional java.time.Instant? activationTime, optional java.time.Instant? expirationTime, optional androidx.privacysandbox.ads.adservices.common.AdSelectionSignals? userBiddingSignals, optional androidx.privacysandbox.ads.adservices.customaudience.TrustedBiddingData? trustedBiddingSignals);
+    method public java.time.Instant? getActivationTime();
+    method public java.util.List<androidx.privacysandbox.ads.adservices.common.AdData> getAds();
+    method public android.net.Uri getBiddingLogicUri();
+    method public androidx.privacysandbox.ads.adservices.common.AdTechIdentifier getBuyer();
+    method public android.net.Uri getDailyUpdateUri();
+    method public java.time.Instant? getExpirationTime();
+    method public String getName();
+    method public androidx.privacysandbox.ads.adservices.customaudience.TrustedBiddingData? getTrustedBiddingSignals();
+    method public androidx.privacysandbox.ads.adservices.common.AdSelectionSignals? getUserBiddingSignals();
+    property public final java.time.Instant? activationTime;
+    property public final java.util.List<androidx.privacysandbox.ads.adservices.common.AdData> ads;
+    property public final android.net.Uri biddingLogicUri;
+    property public final androidx.privacysandbox.ads.adservices.common.AdTechIdentifier buyer;
+    property public final android.net.Uri dailyUpdateUri;
+    property public final java.time.Instant? expirationTime;
+    property public final String name;
+    property public final androidx.privacysandbox.ads.adservices.customaudience.TrustedBiddingData? trustedBiddingSignals;
+    property public final androidx.privacysandbox.ads.adservices.common.AdSelectionSignals? userBiddingSignals;
+  }
+
+  public static final class CustomAudience.Builder {
+    ctor public CustomAudience.Builder(androidx.privacysandbox.ads.adservices.common.AdTechIdentifier buyer, String name, android.net.Uri dailyUpdateUri, android.net.Uri biddingLogicUri, java.util.List<androidx.privacysandbox.ads.adservices.common.AdData> ads);
+    method public androidx.privacysandbox.ads.adservices.customaudience.CustomAudience build();
+    method public androidx.privacysandbox.ads.adservices.customaudience.CustomAudience.Builder setActivationTime(java.time.Instant activationTime);
+    method public androidx.privacysandbox.ads.adservices.customaudience.CustomAudience.Builder setAds(java.util.List<androidx.privacysandbox.ads.adservices.common.AdData> ads);
+    method public androidx.privacysandbox.ads.adservices.customaudience.CustomAudience.Builder setBiddingLogicUri(android.net.Uri biddingLogicUri);
+    method public androidx.privacysandbox.ads.adservices.customaudience.CustomAudience.Builder setBuyer(androidx.privacysandbox.ads.adservices.common.AdTechIdentifier buyer);
+    method public androidx.privacysandbox.ads.adservices.customaudience.CustomAudience.Builder setDailyUpdateUri(android.net.Uri dailyUpdateUri);
+    method public androidx.privacysandbox.ads.adservices.customaudience.CustomAudience.Builder setExpirationTime(java.time.Instant expirationTime);
+    method public androidx.privacysandbox.ads.adservices.customaudience.CustomAudience.Builder setName(String name);
+    method public androidx.privacysandbox.ads.adservices.customaudience.CustomAudience.Builder setTrustedBiddingData(androidx.privacysandbox.ads.adservices.customaudience.TrustedBiddingData trustedBiddingSignals);
+    method public androidx.privacysandbox.ads.adservices.customaudience.CustomAudience.Builder setUserBiddingSignals(androidx.privacysandbox.ads.adservices.common.AdSelectionSignals userBiddingSignals);
+  }
+
+  public abstract class CustomAudienceManager {
+    method @RequiresPermission(android.adservices.common.AdServicesPermissions.ACCESS_ADSERVICES_CUSTOM_AUDIENCE) public abstract suspend Object? joinCustomAudience(androidx.privacysandbox.ads.adservices.customaudience.JoinCustomAudienceRequest request, kotlin.coroutines.Continuation<? super kotlin.Unit>);
+    method @RequiresPermission(android.adservices.common.AdServicesPermissions.ACCESS_ADSERVICES_CUSTOM_AUDIENCE) public abstract suspend Object? leaveCustomAudience(androidx.privacysandbox.ads.adservices.customaudience.LeaveCustomAudienceRequest request, kotlin.coroutines.Continuation<? super kotlin.Unit>);
+    method public static final androidx.privacysandbox.ads.adservices.customaudience.CustomAudienceManager? obtain(android.content.Context context);
+    field public static final androidx.privacysandbox.ads.adservices.customaudience.CustomAudienceManager.Companion Companion;
+  }
+
+  public static final class CustomAudienceManager.Companion {
+    method public androidx.privacysandbox.ads.adservices.customaudience.CustomAudienceManager? obtain(android.content.Context context);
+  }
+
+  public final class JoinCustomAudienceRequest {
+    ctor public JoinCustomAudienceRequest(androidx.privacysandbox.ads.adservices.customaudience.CustomAudience customAudience);
+    method public androidx.privacysandbox.ads.adservices.customaudience.CustomAudience getCustomAudience();
+    property public final androidx.privacysandbox.ads.adservices.customaudience.CustomAudience customAudience;
+  }
+
+  public final class LeaveCustomAudienceRequest {
+    ctor public LeaveCustomAudienceRequest(androidx.privacysandbox.ads.adservices.common.AdTechIdentifier buyer, String name);
+    method public androidx.privacysandbox.ads.adservices.common.AdTechIdentifier getBuyer();
+    method public String getName();
+    property public final androidx.privacysandbox.ads.adservices.common.AdTechIdentifier buyer;
+    property public final String name;
+  }
+
+  public final class TrustedBiddingData {
+    ctor public TrustedBiddingData(android.net.Uri trustedBiddingUri, java.util.List<java.lang.String> trustedBiddingKeys);
+    method public java.util.List<java.lang.String> getTrustedBiddingKeys();
+    method public android.net.Uri getTrustedBiddingUri();
+    property public final java.util.List<java.lang.String> trustedBiddingKeys;
+    property public final android.net.Uri trustedBiddingUri;
+  }
+
+}
+
+package androidx.privacysandbox.ads.adservices.measurement {
+
+  @RequiresApi(android.os.Build.VERSION_CODES.TIRAMISU) public final class DeletionRequest {
+    ctor public DeletionRequest(int deletionMode, int matchBehavior, optional java.time.Instant start, optional java.time.Instant end, optional java.util.List<? extends android.net.Uri> domainUris, optional java.util.List<? extends android.net.Uri> originUris);
+    method public int getDeletionMode();
+    method public java.util.List<android.net.Uri> getDomainUris();
+    method public java.time.Instant getEnd();
+    method public int getMatchBehavior();
+    method public java.util.List<android.net.Uri> getOriginUris();
+    method public java.time.Instant getStart();
+    property public final int deletionMode;
+    property public final java.util.List<android.net.Uri> domainUris;
+    property public final java.time.Instant end;
+    property public final int matchBehavior;
+    property public final java.util.List<android.net.Uri> originUris;
+    property public final java.time.Instant start;
+    field public static final androidx.privacysandbox.ads.adservices.measurement.DeletionRequest.Companion Companion;
+    field public static final int DELETION_MODE_ALL = 0; // 0x0
+    field public static final int DELETION_MODE_EXCLUDE_INTERNAL_DATA = 1; // 0x1
+    field public static final int MATCH_BEHAVIOR_DELETE = 0; // 0x0
+    field public static final int MATCH_BEHAVIOR_PRESERVE = 1; // 0x1
+  }
+
+  @RequiresApi(android.os.Build.VERSION_CODES.TIRAMISU) public static final class DeletionRequest.Builder {
+    ctor public DeletionRequest.Builder(int deletionMode, int matchBehavior);
+    method public androidx.privacysandbox.ads.adservices.measurement.DeletionRequest build();
+    method public androidx.privacysandbox.ads.adservices.measurement.DeletionRequest.Builder setDomainUris(java.util.List<? extends android.net.Uri> domainUris);
+    method public androidx.privacysandbox.ads.adservices.measurement.DeletionRequest.Builder setEnd(java.time.Instant end);
+    method public androidx.privacysandbox.ads.adservices.measurement.DeletionRequest.Builder setOriginUris(java.util.List<? extends android.net.Uri> originUris);
+    method public androidx.privacysandbox.ads.adservices.measurement.DeletionRequest.Builder setStart(java.time.Instant start);
+  }
+
+  public static final class DeletionRequest.Companion {
+  }
+
+  public abstract class MeasurementManager {
+    ctor public MeasurementManager();
+    method public abstract suspend Object? deleteRegistrations(androidx.privacysandbox.ads.adservices.measurement.DeletionRequest deletionRequest, kotlin.coroutines.Continuation<? super kotlin.Unit>);
+    method @RequiresPermission(android.adservices.common.AdServicesPermissions.ACCESS_ADSERVICES_ATTRIBUTION) public abstract suspend Object? getMeasurementApiStatus(kotlin.coroutines.Continuation<? super java.lang.Integer>);
+    method public static final androidx.privacysandbox.ads.adservices.measurement.MeasurementManager? obtain(android.content.Context context);
+    method @RequiresPermission(android.adservices.common.AdServicesPermissions.ACCESS_ADSERVICES_ATTRIBUTION) public abstract suspend Object? registerSource(android.net.Uri attributionSource, android.view.InputEvent? inputEvent, kotlin.coroutines.Continuation<? super kotlin.Unit>);
+    method @RequiresPermission(android.adservices.common.AdServicesPermissions.ACCESS_ADSERVICES_ATTRIBUTION) public abstract suspend Object? registerTrigger(android.net.Uri trigger, kotlin.coroutines.Continuation<? super kotlin.Unit>);
+    method @RequiresPermission(android.adservices.common.AdServicesPermissions.ACCESS_ADSERVICES_ATTRIBUTION) public abstract suspend Object? registerWebSource(androidx.privacysandbox.ads.adservices.measurement.WebSourceRegistrationRequest request, kotlin.coroutines.Continuation<? super kotlin.Unit>);
+    method @RequiresPermission(android.adservices.common.AdServicesPermissions.ACCESS_ADSERVICES_ATTRIBUTION) public abstract suspend Object? registerWebTrigger(androidx.privacysandbox.ads.adservices.measurement.WebTriggerRegistrationRequest request, kotlin.coroutines.Continuation<? super kotlin.Unit>);
+    field public static final androidx.privacysandbox.ads.adservices.measurement.MeasurementManager.Companion Companion;
+    field public static final int MEASUREMENT_API_STATE_DISABLED = 0; // 0x0
+    field public static final int MEASUREMENT_API_STATE_ENABLED = 1; // 0x1
+  }
+
+  public static final class MeasurementManager.Companion {
+    method public androidx.privacysandbox.ads.adservices.measurement.MeasurementManager? obtain(android.content.Context context);
+  }
+
+  @RequiresApi(android.os.Build.VERSION_CODES.TIRAMISU) public final class WebSourceParams {
+    ctor public WebSourceParams(android.net.Uri registrationUri, boolean debugKeyAllowed);
+    method public boolean getDebugKeyAllowed();
+    method public android.net.Uri getRegistrationUri();
+    property public final boolean debugKeyAllowed;
+    property public final android.net.Uri registrationUri;
+  }
+
+  @RequiresApi(android.os.Build.VERSION_CODES.TIRAMISU) public final class WebSourceRegistrationRequest {
+    ctor public WebSourceRegistrationRequest(java.util.List<androidx.privacysandbox.ads.adservices.measurement.WebSourceParams> webSourceParams, android.net.Uri topOriginUri, optional android.view.InputEvent? inputEvent, optional android.net.Uri? appDestination, optional android.net.Uri? webDestination, optional android.net.Uri? verifiedDestination);
+    method public android.net.Uri? getAppDestination();
+    method public android.view.InputEvent? getInputEvent();
+    method public android.net.Uri getTopOriginUri();
+    method public android.net.Uri? getVerifiedDestination();
+    method public android.net.Uri? getWebDestination();
+    method public java.util.List<androidx.privacysandbox.ads.adservices.measurement.WebSourceParams> getWebSourceParams();
+    property public final android.net.Uri? appDestination;
+    property public final android.view.InputEvent? inputEvent;
+    property public final android.net.Uri topOriginUri;
+    property public final android.net.Uri? verifiedDestination;
+    property public final android.net.Uri? webDestination;
+    property public final java.util.List<androidx.privacysandbox.ads.adservices.measurement.WebSourceParams> webSourceParams;
+  }
+
+  public static final class WebSourceRegistrationRequest.Builder {
+    ctor public WebSourceRegistrationRequest.Builder(java.util.List<androidx.privacysandbox.ads.adservices.measurement.WebSourceParams> webSourceParams, android.net.Uri topOriginUri);
+    method public androidx.privacysandbox.ads.adservices.measurement.WebSourceRegistrationRequest build();
+    method public androidx.privacysandbox.ads.adservices.measurement.WebSourceRegistrationRequest.Builder setAppDestination(android.net.Uri? appDestination);
+    method public androidx.privacysandbox.ads.adservices.measurement.WebSourceRegistrationRequest.Builder setInputEvent(android.view.InputEvent inputEvent);
+    method public androidx.privacysandbox.ads.adservices.measurement.WebSourceRegistrationRequest.Builder setVerifiedDestination(android.net.Uri? verifiedDestination);
+    method public androidx.privacysandbox.ads.adservices.measurement.WebSourceRegistrationRequest.Builder setWebDestination(android.net.Uri? webDestination);
+  }
+
+  @RequiresApi(android.os.Build.VERSION_CODES.TIRAMISU) public final class WebTriggerParams {
+    ctor public WebTriggerParams(android.net.Uri registrationUri, boolean debugKeyAllowed);
+    method public boolean getDebugKeyAllowed();
+    method public android.net.Uri getRegistrationUri();
+    property public final boolean debugKeyAllowed;
+    property public final android.net.Uri registrationUri;
+  }
+
+  @RequiresApi(android.os.Build.VERSION_CODES.TIRAMISU) public final class WebTriggerRegistrationRequest {
+    ctor public WebTriggerRegistrationRequest(java.util.List<androidx.privacysandbox.ads.adservices.measurement.WebTriggerParams> webTriggerParams, android.net.Uri destination);
+    method public android.net.Uri getDestination();
+    method public java.util.List<androidx.privacysandbox.ads.adservices.measurement.WebTriggerParams> getWebTriggerParams();
+    property public final android.net.Uri destination;
+    property public final java.util.List<androidx.privacysandbox.ads.adservices.measurement.WebTriggerParams> webTriggerParams;
+  }
+
+}
+
+package androidx.privacysandbox.ads.adservices.topics {
+
+  public final class GetTopicsRequest {
+    ctor public GetTopicsRequest(optional String sdkName, optional boolean shouldRecordObservation);
+    method public String getSdkName();
+    method public boolean getShouldRecordObservation();
+    property public final String sdkName;
+    property public final boolean shouldRecordObservation;
+  }
+
+  public static final class GetTopicsRequest.Builder {
+    ctor public GetTopicsRequest.Builder();
+    method public androidx.privacysandbox.ads.adservices.topics.GetTopicsRequest build();
+    method public androidx.privacysandbox.ads.adservices.topics.GetTopicsRequest.Builder setSdkName(String sdkName);
+    method public androidx.privacysandbox.ads.adservices.topics.GetTopicsRequest.Builder setShouldRecordObservation(boolean shouldRecordObservation);
+  }
+
+  public final class GetTopicsResponse {
+    ctor public GetTopicsResponse(java.util.List<androidx.privacysandbox.ads.adservices.topics.Topic> topics);
+    method public java.util.List<androidx.privacysandbox.ads.adservices.topics.Topic> getTopics();
+    property public final java.util.List<androidx.privacysandbox.ads.adservices.topics.Topic> topics;
+  }
+
+  public final class Topic {
+    ctor public Topic(long taxonomyVersion, long modelVersion, int topicId);
+    method public long getModelVersion();
+    method public long getTaxonomyVersion();
+    method public int getTopicId();
+    property public final long modelVersion;
+    property public final long taxonomyVersion;
+    property public final int topicId;
+  }
+
+  public abstract class TopicsManager {
+    method @RequiresPermission(android.adservices.common.AdServicesPermissions.ACCESS_ADSERVICES_TOPICS) public abstract suspend Object? getTopics(androidx.privacysandbox.ads.adservices.topics.GetTopicsRequest request, kotlin.coroutines.Continuation<? super androidx.privacysandbox.ads.adservices.topics.GetTopicsResponse>);
+    method public static final androidx.privacysandbox.ads.adservices.topics.TopicsManager? obtain(android.content.Context context);
+    field public static final androidx.privacysandbox.ads.adservices.topics.TopicsManager.Companion Companion;
+  }
+
+  public static final class TopicsManager.Companion {
+    method public androidx.privacysandbox.ads.adservices.topics.TopicsManager? obtain(android.content.Context context);
+  }
+
+}
+
diff --git a/privacysandbox/ads/ads-adservices/api/res-current.txt b/privacysandbox/ads/ads-adservices/api/res-current.txt
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/privacysandbox/ads/ads-adservices/api/res-current.txt
diff --git a/privacysandbox/ads/ads-adservices/api/restricted_current.txt b/privacysandbox/ads/ads-adservices/api/restricted_current.txt
new file mode 100644
index 0000000..01aa5c6
--- /dev/null
+++ b/privacysandbox/ads/ads-adservices/api/restricted_current.txt
@@ -0,0 +1,345 @@
+// Signature format: 4.0
+package androidx.privacysandbox.ads.adservices.adid {
+
+  public final class AdId {
+    method public String getAdId();
+    method public boolean isLimitAdTrackingEnabled();
+    property public final String adId;
+    property public final boolean isLimitAdTrackingEnabled;
+  }
+
+  public abstract class AdIdManager {
+    method @RequiresPermission(android.adservices.common.AdServicesPermissions.ACCESS_ADSERVICES_AD_ID) public abstract suspend Object? getAdId(kotlin.coroutines.Continuation<? super androidx.privacysandbox.ads.adservices.adid.AdId>);
+    method public static final androidx.privacysandbox.ads.adservices.adid.AdIdManager? obtain(android.content.Context context);
+    field public static final androidx.privacysandbox.ads.adservices.adid.AdIdManager.Companion Companion;
+  }
+
+  public static final class AdIdManager.Companion {
+    method public androidx.privacysandbox.ads.adservices.adid.AdIdManager? obtain(android.content.Context context);
+  }
+
+}
+
+package androidx.privacysandbox.ads.adservices.adselection {
+
+  public final class AdSelectionConfig {
+    ctor public AdSelectionConfig(androidx.privacysandbox.ads.adservices.common.AdTechIdentifier seller, android.net.Uri decisionLogicUri, java.util.List<androidx.privacysandbox.ads.adservices.common.AdTechIdentifier> customAudienceBuyers, androidx.privacysandbox.ads.adservices.common.AdSelectionSignals adSelectionSignals, androidx.privacysandbox.ads.adservices.common.AdSelectionSignals sellerSignals, java.util.Map<androidx.privacysandbox.ads.adservices.common.AdTechIdentifier,androidx.privacysandbox.ads.adservices.common.AdSelectionSignals> perBuyerSignals, android.net.Uri trustedScoringSignalsUri);
+    method public androidx.privacysandbox.ads.adservices.common.AdSelectionSignals getAdSelectionSignals();
+    method public java.util.List<androidx.privacysandbox.ads.adservices.common.AdTechIdentifier> getCustomAudienceBuyers();
+    method public android.net.Uri getDecisionLogicUri();
+    method public java.util.Map<androidx.privacysandbox.ads.adservices.common.AdTechIdentifier,androidx.privacysandbox.ads.adservices.common.AdSelectionSignals> getPerBuyerSignals();
+    method public androidx.privacysandbox.ads.adservices.common.AdTechIdentifier getSeller();
+    method public androidx.privacysandbox.ads.adservices.common.AdSelectionSignals getSellerSignals();
+    method public android.net.Uri getTrustedScoringSignalsUri();
+    property public final androidx.privacysandbox.ads.adservices.common.AdSelectionSignals adSelectionSignals;
+    property public final java.util.List<androidx.privacysandbox.ads.adservices.common.AdTechIdentifier> customAudienceBuyers;
+    property public final android.net.Uri decisionLogicUri;
+    property public final java.util.Map<androidx.privacysandbox.ads.adservices.common.AdTechIdentifier,androidx.privacysandbox.ads.adservices.common.AdSelectionSignals> perBuyerSignals;
+    property public final androidx.privacysandbox.ads.adservices.common.AdTechIdentifier seller;
+    property public final androidx.privacysandbox.ads.adservices.common.AdSelectionSignals sellerSignals;
+    property public final android.net.Uri trustedScoringSignalsUri;
+  }
+
+  public abstract class AdSelectionManager {
+    method public static final androidx.privacysandbox.ads.adservices.adselection.AdSelectionManager? obtain(android.content.Context context);
+    method @RequiresPermission(android.adservices.common.AdServicesPermissions.ACCESS_ADSERVICES_CUSTOM_AUDIENCE) public abstract suspend Object? reportImpression(androidx.privacysandbox.ads.adservices.adselection.ReportImpressionRequest reportImpressionRequest, kotlin.coroutines.Continuation<? super kotlin.Unit>);
+    method @RequiresPermission(android.adservices.common.AdServicesPermissions.ACCESS_ADSERVICES_CUSTOM_AUDIENCE) public abstract suspend Object? selectAds(androidx.privacysandbox.ads.adservices.adselection.AdSelectionConfig adSelectionConfig, kotlin.coroutines.Continuation<? super androidx.privacysandbox.ads.adservices.adselection.AdSelectionOutcome>);
+    field public static final androidx.privacysandbox.ads.adservices.adselection.AdSelectionManager.Companion Companion;
+  }
+
+  public static final class AdSelectionManager.Companion {
+    method public androidx.privacysandbox.ads.adservices.adselection.AdSelectionManager? obtain(android.content.Context context);
+  }
+
+  public final class AdSelectionOutcome {
+    ctor public AdSelectionOutcome(long adSelectionId, android.net.Uri renderUri);
+    method public long getAdSelectionId();
+    method public android.net.Uri getRenderUri();
+    property public final long adSelectionId;
+    property public final android.net.Uri renderUri;
+  }
+
+  public final class ReportImpressionRequest {
+    ctor public ReportImpressionRequest(long adSelectionId, androidx.privacysandbox.ads.adservices.adselection.AdSelectionConfig adSelectionConfig);
+    method public androidx.privacysandbox.ads.adservices.adselection.AdSelectionConfig getAdSelectionConfig();
+    method public long getAdSelectionId();
+    property public final androidx.privacysandbox.ads.adservices.adselection.AdSelectionConfig adSelectionConfig;
+    property public final long adSelectionId;
+  }
+
+}
+
+package androidx.privacysandbox.ads.adservices.appsetid {
+
+  public final class AppSetId {
+    ctor public AppSetId(String id, int scope);
+    method public String getId();
+    method public int getScope();
+    property public final String id;
+    property public final int scope;
+    field public static final androidx.privacysandbox.ads.adservices.appsetid.AppSetId.Companion Companion;
+    field public static final int SCOPE_APP = 1; // 0x1
+    field public static final int SCOPE_DEVELOPER = 2; // 0x2
+  }
+
+  public static final class AppSetId.Companion {
+  }
+
+  public abstract class AppSetIdManager {
+    method public abstract suspend Object? getAppSetId(kotlin.coroutines.Continuation<? super androidx.privacysandbox.ads.adservices.appsetid.AppSetId>);
+    method public static final androidx.privacysandbox.ads.adservices.appsetid.AppSetIdManager? obtain(android.content.Context context);
+    field public static final androidx.privacysandbox.ads.adservices.appsetid.AppSetIdManager.Companion Companion;
+  }
+
+  public static final class AppSetIdManager.Companion {
+    method public androidx.privacysandbox.ads.adservices.appsetid.AppSetIdManager? obtain(android.content.Context context);
+  }
+
+}
+
+package androidx.privacysandbox.ads.adservices.common {
+
+  public final class AdData {
+    ctor public AdData(android.net.Uri renderUri, String metadata);
+    method public String getMetadata();
+    method public android.net.Uri getRenderUri();
+    property public final String metadata;
+    property public final android.net.Uri renderUri;
+  }
+
+  public final class AdSelectionSignals {
+    ctor public AdSelectionSignals(String signals);
+    method public String getSignals();
+    property public final String signals;
+  }
+
+  public final class AdTechIdentifier {
+    ctor public AdTechIdentifier(String identifier);
+    method public String getIdentifier();
+    property public final String identifier;
+  }
+
+}
+
+package androidx.privacysandbox.ads.adservices.customaudience {
+
+  public final class CustomAudience {
+    ctor public CustomAudience(androidx.privacysandbox.ads.adservices.common.AdTechIdentifier buyer, String name, android.net.Uri dailyUpdateUri, android.net.Uri biddingLogicUri, java.util.List<androidx.privacysandbox.ads.adservices.common.AdData> ads, optional java.time.Instant? activationTime, optional java.time.Instant? expirationTime, optional androidx.privacysandbox.ads.adservices.common.AdSelectionSignals? userBiddingSignals, optional androidx.privacysandbox.ads.adservices.customaudience.TrustedBiddingData? trustedBiddingSignals);
+    method public java.time.Instant? getActivationTime();
+    method public java.util.List<androidx.privacysandbox.ads.adservices.common.AdData> getAds();
+    method public android.net.Uri getBiddingLogicUri();
+    method public androidx.privacysandbox.ads.adservices.common.AdTechIdentifier getBuyer();
+    method public android.net.Uri getDailyUpdateUri();
+    method public java.time.Instant? getExpirationTime();
+    method public String getName();
+    method public androidx.privacysandbox.ads.adservices.customaudience.TrustedBiddingData? getTrustedBiddingSignals();
+    method public androidx.privacysandbox.ads.adservices.common.AdSelectionSignals? getUserBiddingSignals();
+    property public final java.time.Instant? activationTime;
+    property public final java.util.List<androidx.privacysandbox.ads.adservices.common.AdData> ads;
+    property public final android.net.Uri biddingLogicUri;
+    property public final androidx.privacysandbox.ads.adservices.common.AdTechIdentifier buyer;
+    property public final android.net.Uri dailyUpdateUri;
+    property public final java.time.Instant? expirationTime;
+    property public final String name;
+    property public final androidx.privacysandbox.ads.adservices.customaudience.TrustedBiddingData? trustedBiddingSignals;
+    property public final androidx.privacysandbox.ads.adservices.common.AdSelectionSignals? userBiddingSignals;
+  }
+
+  public static final class CustomAudience.Builder {
+    ctor public CustomAudience.Builder(androidx.privacysandbox.ads.adservices.common.AdTechIdentifier buyer, String name, android.net.Uri dailyUpdateUri, android.net.Uri biddingLogicUri, java.util.List<androidx.privacysandbox.ads.adservices.common.AdData> ads);
+    method public androidx.privacysandbox.ads.adservices.customaudience.CustomAudience build();
+    method public androidx.privacysandbox.ads.adservices.customaudience.CustomAudience.Builder setActivationTime(java.time.Instant activationTime);
+    method public androidx.privacysandbox.ads.adservices.customaudience.CustomAudience.Builder setAds(java.util.List<androidx.privacysandbox.ads.adservices.common.AdData> ads);
+    method public androidx.privacysandbox.ads.adservices.customaudience.CustomAudience.Builder setBiddingLogicUri(android.net.Uri biddingLogicUri);
+    method public androidx.privacysandbox.ads.adservices.customaudience.CustomAudience.Builder setBuyer(androidx.privacysandbox.ads.adservices.common.AdTechIdentifier buyer);
+    method public androidx.privacysandbox.ads.adservices.customaudience.CustomAudience.Builder setDailyUpdateUri(android.net.Uri dailyUpdateUri);
+    method public androidx.privacysandbox.ads.adservices.customaudience.CustomAudience.Builder setExpirationTime(java.time.Instant expirationTime);
+    method public androidx.privacysandbox.ads.adservices.customaudience.CustomAudience.Builder setName(String name);
+    method public androidx.privacysandbox.ads.adservices.customaudience.CustomAudience.Builder setTrustedBiddingData(androidx.privacysandbox.ads.adservices.customaudience.TrustedBiddingData trustedBiddingSignals);
+    method public androidx.privacysandbox.ads.adservices.customaudience.CustomAudience.Builder setUserBiddingSignals(androidx.privacysandbox.ads.adservices.common.AdSelectionSignals userBiddingSignals);
+  }
+
+  public abstract class CustomAudienceManager {
+    method @RequiresPermission(android.adservices.common.AdServicesPermissions.ACCESS_ADSERVICES_CUSTOM_AUDIENCE) public abstract suspend Object? joinCustomAudience(androidx.privacysandbox.ads.adservices.customaudience.JoinCustomAudienceRequest request, kotlin.coroutines.Continuation<? super kotlin.Unit>);
+    method @RequiresPermission(android.adservices.common.AdServicesPermissions.ACCESS_ADSERVICES_CUSTOM_AUDIENCE) public abstract suspend Object? leaveCustomAudience(androidx.privacysandbox.ads.adservices.customaudience.LeaveCustomAudienceRequest request, kotlin.coroutines.Continuation<? super kotlin.Unit>);
+    method public static final androidx.privacysandbox.ads.adservices.customaudience.CustomAudienceManager? obtain(android.content.Context context);
+    field public static final androidx.privacysandbox.ads.adservices.customaudience.CustomAudienceManager.Companion Companion;
+  }
+
+  public static final class CustomAudienceManager.Companion {
+    method public androidx.privacysandbox.ads.adservices.customaudience.CustomAudienceManager? obtain(android.content.Context context);
+  }
+
+  public final class JoinCustomAudienceRequest {
+    ctor public JoinCustomAudienceRequest(androidx.privacysandbox.ads.adservices.customaudience.CustomAudience customAudience);
+    method public androidx.privacysandbox.ads.adservices.customaudience.CustomAudience getCustomAudience();
+    property public final androidx.privacysandbox.ads.adservices.customaudience.CustomAudience customAudience;
+  }
+
+  public final class LeaveCustomAudienceRequest {
+    ctor public LeaveCustomAudienceRequest(androidx.privacysandbox.ads.adservices.common.AdTechIdentifier buyer, String name);
+    method public androidx.privacysandbox.ads.adservices.common.AdTechIdentifier getBuyer();
+    method public String getName();
+    property public final androidx.privacysandbox.ads.adservices.common.AdTechIdentifier buyer;
+    property public final String name;
+  }
+
+  public final class TrustedBiddingData {
+    ctor public TrustedBiddingData(android.net.Uri trustedBiddingUri, java.util.List<java.lang.String> trustedBiddingKeys);
+    method public java.util.List<java.lang.String> getTrustedBiddingKeys();
+    method public android.net.Uri getTrustedBiddingUri();
+    property public final java.util.List<java.lang.String> trustedBiddingKeys;
+    property public final android.net.Uri trustedBiddingUri;
+  }
+
+}
+
+package androidx.privacysandbox.ads.adservices.measurement {
+
+  @RequiresApi(android.os.Build.VERSION_CODES.TIRAMISU) public final class DeletionRequest {
+    ctor public DeletionRequest(int deletionMode, int matchBehavior, optional java.time.Instant start, optional java.time.Instant end, optional java.util.List<? extends android.net.Uri> domainUris, optional java.util.List<? extends android.net.Uri> originUris);
+    method public int getDeletionMode();
+    method public java.util.List<android.net.Uri> getDomainUris();
+    method public java.time.Instant getEnd();
+    method public int getMatchBehavior();
+    method public java.util.List<android.net.Uri> getOriginUris();
+    method public java.time.Instant getStart();
+    property public final int deletionMode;
+    property public final java.util.List<android.net.Uri> domainUris;
+    property public final java.time.Instant end;
+    property public final int matchBehavior;
+    property public final java.util.List<android.net.Uri> originUris;
+    property public final java.time.Instant start;
+    field public static final androidx.privacysandbox.ads.adservices.measurement.DeletionRequest.Companion Companion;
+    field public static final int DELETION_MODE_ALL = 0; // 0x0
+    field public static final int DELETION_MODE_EXCLUDE_INTERNAL_DATA = 1; // 0x1
+    field public static final int MATCH_BEHAVIOR_DELETE = 0; // 0x0
+    field public static final int MATCH_BEHAVIOR_PRESERVE = 1; // 0x1
+  }
+
+  @RequiresApi(android.os.Build.VERSION_CODES.TIRAMISU) public static final class DeletionRequest.Builder {
+    ctor public DeletionRequest.Builder(int deletionMode, int matchBehavior);
+    method public androidx.privacysandbox.ads.adservices.measurement.DeletionRequest build();
+    method public androidx.privacysandbox.ads.adservices.measurement.DeletionRequest.Builder setDomainUris(java.util.List<? extends android.net.Uri> domainUris);
+    method public androidx.privacysandbox.ads.adservices.measurement.DeletionRequest.Builder setEnd(java.time.Instant end);
+    method public androidx.privacysandbox.ads.adservices.measurement.DeletionRequest.Builder setOriginUris(java.util.List<? extends android.net.Uri> originUris);
+    method public androidx.privacysandbox.ads.adservices.measurement.DeletionRequest.Builder setStart(java.time.Instant start);
+  }
+
+  public static final class DeletionRequest.Companion {
+  }
+
+  public abstract class MeasurementManager {
+    ctor public MeasurementManager();
+    method public abstract suspend Object? deleteRegistrations(androidx.privacysandbox.ads.adservices.measurement.DeletionRequest deletionRequest, kotlin.coroutines.Continuation<? super kotlin.Unit>);
+    method @RequiresPermission(android.adservices.common.AdServicesPermissions.ACCESS_ADSERVICES_ATTRIBUTION) public abstract suspend Object? getMeasurementApiStatus(kotlin.coroutines.Continuation<? super java.lang.Integer>);
+    method public static final androidx.privacysandbox.ads.adservices.measurement.MeasurementManager? obtain(android.content.Context context);
+    method @RequiresPermission(android.adservices.common.AdServicesPermissions.ACCESS_ADSERVICES_ATTRIBUTION) public abstract suspend Object? registerSource(android.net.Uri attributionSource, android.view.InputEvent? inputEvent, kotlin.coroutines.Continuation<? super kotlin.Unit>);
+    method @RequiresPermission(android.adservices.common.AdServicesPermissions.ACCESS_ADSERVICES_ATTRIBUTION) public abstract suspend Object? registerTrigger(android.net.Uri trigger, kotlin.coroutines.Continuation<? super kotlin.Unit>);
+    method @RequiresPermission(android.adservices.common.AdServicesPermissions.ACCESS_ADSERVICES_ATTRIBUTION) public abstract suspend Object? registerWebSource(androidx.privacysandbox.ads.adservices.measurement.WebSourceRegistrationRequest request, kotlin.coroutines.Continuation<? super kotlin.Unit>);
+    method @RequiresPermission(android.adservices.common.AdServicesPermissions.ACCESS_ADSERVICES_ATTRIBUTION) public abstract suspend Object? registerWebTrigger(androidx.privacysandbox.ads.adservices.measurement.WebTriggerRegistrationRequest request, kotlin.coroutines.Continuation<? super kotlin.Unit>);
+    field public static final androidx.privacysandbox.ads.adservices.measurement.MeasurementManager.Companion Companion;
+    field public static final int MEASUREMENT_API_STATE_DISABLED = 0; // 0x0
+    field public static final int MEASUREMENT_API_STATE_ENABLED = 1; // 0x1
+  }
+
+  public static final class MeasurementManager.Companion {
+    method public androidx.privacysandbox.ads.adservices.measurement.MeasurementManager? obtain(android.content.Context context);
+  }
+
+  @RequiresApi(android.os.Build.VERSION_CODES.TIRAMISU) public final class WebSourceParams {
+    ctor public WebSourceParams(android.net.Uri registrationUri, boolean debugKeyAllowed);
+    method public boolean getDebugKeyAllowed();
+    method public android.net.Uri getRegistrationUri();
+    property public final boolean debugKeyAllowed;
+    property public final android.net.Uri registrationUri;
+  }
+
+  @RequiresApi(android.os.Build.VERSION_CODES.TIRAMISU) public final class WebSourceRegistrationRequest {
+    ctor public WebSourceRegistrationRequest(java.util.List<androidx.privacysandbox.ads.adservices.measurement.WebSourceParams> webSourceParams, android.net.Uri topOriginUri, optional android.view.InputEvent? inputEvent, optional android.net.Uri? appDestination, optional android.net.Uri? webDestination, optional android.net.Uri? verifiedDestination);
+    method public android.net.Uri? getAppDestination();
+    method public android.view.InputEvent? getInputEvent();
+    method public android.net.Uri getTopOriginUri();
+    method public android.net.Uri? getVerifiedDestination();
+    method public android.net.Uri? getWebDestination();
+    method public java.util.List<androidx.privacysandbox.ads.adservices.measurement.WebSourceParams> getWebSourceParams();
+    property public final android.net.Uri? appDestination;
+    property public final android.view.InputEvent? inputEvent;
+    property public final android.net.Uri topOriginUri;
+    property public final android.net.Uri? verifiedDestination;
+    property public final android.net.Uri? webDestination;
+    property public final java.util.List<androidx.privacysandbox.ads.adservices.measurement.WebSourceParams> webSourceParams;
+  }
+
+  public static final class WebSourceRegistrationRequest.Builder {
+    ctor public WebSourceRegistrationRequest.Builder(java.util.List<androidx.privacysandbox.ads.adservices.measurement.WebSourceParams> webSourceParams, android.net.Uri topOriginUri);
+    method public androidx.privacysandbox.ads.adservices.measurement.WebSourceRegistrationRequest build();
+    method public androidx.privacysandbox.ads.adservices.measurement.WebSourceRegistrationRequest.Builder setAppDestination(android.net.Uri? appDestination);
+    method public androidx.privacysandbox.ads.adservices.measurement.WebSourceRegistrationRequest.Builder setInputEvent(android.view.InputEvent inputEvent);
+    method public androidx.privacysandbox.ads.adservices.measurement.WebSourceRegistrationRequest.Builder setVerifiedDestination(android.net.Uri? verifiedDestination);
+    method public androidx.privacysandbox.ads.adservices.measurement.WebSourceRegistrationRequest.Builder setWebDestination(android.net.Uri? webDestination);
+  }
+
+  @RequiresApi(android.os.Build.VERSION_CODES.TIRAMISU) public final class WebTriggerParams {
+    ctor public WebTriggerParams(android.net.Uri registrationUri, boolean debugKeyAllowed);
+    method public boolean getDebugKeyAllowed();
+    method public android.net.Uri getRegistrationUri();
+    property public final boolean debugKeyAllowed;
+    property public final android.net.Uri registrationUri;
+  }
+
+  @RequiresApi(android.os.Build.VERSION_CODES.TIRAMISU) public final class WebTriggerRegistrationRequest {
+    ctor public WebTriggerRegistrationRequest(java.util.List<androidx.privacysandbox.ads.adservices.measurement.WebTriggerParams> webTriggerParams, android.net.Uri destination);
+    method public android.net.Uri getDestination();
+    method public java.util.List<androidx.privacysandbox.ads.adservices.measurement.WebTriggerParams> getWebTriggerParams();
+    property public final android.net.Uri destination;
+    property public final java.util.List<androidx.privacysandbox.ads.adservices.measurement.WebTriggerParams> webTriggerParams;
+  }
+
+}
+
+package androidx.privacysandbox.ads.adservices.topics {
+
+  public final class GetTopicsRequest {
+    ctor public GetTopicsRequest(optional String sdkName, optional boolean shouldRecordObservation);
+    method public String getSdkName();
+    method public boolean getShouldRecordObservation();
+    property public final String sdkName;
+    property public final boolean shouldRecordObservation;
+  }
+
+  public static final class GetTopicsRequest.Builder {
+    ctor public GetTopicsRequest.Builder();
+    method public androidx.privacysandbox.ads.adservices.topics.GetTopicsRequest build();
+    method public androidx.privacysandbox.ads.adservices.topics.GetTopicsRequest.Builder setSdkName(String sdkName);
+    method public androidx.privacysandbox.ads.adservices.topics.GetTopicsRequest.Builder setShouldRecordObservation(boolean shouldRecordObservation);
+  }
+
+  public final class GetTopicsResponse {
+    ctor public GetTopicsResponse(java.util.List<androidx.privacysandbox.ads.adservices.topics.Topic> topics);
+    method public java.util.List<androidx.privacysandbox.ads.adservices.topics.Topic> getTopics();
+    property public final java.util.List<androidx.privacysandbox.ads.adservices.topics.Topic> topics;
+  }
+
+  public final class Topic {
+    ctor public Topic(long taxonomyVersion, long modelVersion, int topicId);
+    method public long getModelVersion();
+    method public long getTaxonomyVersion();
+    method public int getTopicId();
+    property public final long modelVersion;
+    property public final long taxonomyVersion;
+    property public final int topicId;
+  }
+
+  public abstract class TopicsManager {
+    method @RequiresPermission(android.adservices.common.AdServicesPermissions.ACCESS_ADSERVICES_TOPICS) public abstract suspend Object? getTopics(androidx.privacysandbox.ads.adservices.topics.GetTopicsRequest request, kotlin.coroutines.Continuation<? super androidx.privacysandbox.ads.adservices.topics.GetTopicsResponse>);
+    method public static final androidx.privacysandbox.ads.adservices.topics.TopicsManager? obtain(android.content.Context context);
+    field public static final androidx.privacysandbox.ads.adservices.topics.TopicsManager.Companion Companion;
+  }
+
+  public static final class TopicsManager.Companion {
+    method public androidx.privacysandbox.ads.adservices.topics.TopicsManager? obtain(android.content.Context context);
+  }
+
+}
+
diff --git a/privacysandbox/ads/ads-adservices/build.gradle b/privacysandbox/ads/ads-adservices/build.gradle
new file mode 100644
index 0000000..2679d8e
--- /dev/null
+++ b/privacysandbox/ads/ads-adservices/build.gradle
@@ -0,0 +1,58 @@
+/*
+ * Copyright (C) 2022 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+import androidx.build.LibraryType
+import androidx.build.Publish
+import androidx.build.RunApiTasks
+
+plugins {
+    id("AndroidXPlugin")
+    id("com.android.library")
+    id("org.jetbrains.kotlin.android")
+}
+
+dependencies {
+    api(libs.kotlinStdlib)
+    api(libs.kotlinCoroutinesCore)
+    implementation("androidx.core:core-ktx:1.8.0")
+    api(projectOrArtifact(":annotation:annotation"))
+
+    androidTestImplementation(libs.junit)
+    androidTestImplementation(libs.kotlinTestJunit)
+    androidTestImplementation(libs.testCore)
+    androidTestImplementation(libs.testExtJunit)
+    androidTestImplementation(libs.testRunner)
+    androidTestImplementation(libs.testRules)
+    androidTestImplementation(libs.truth)
+    androidTestImplementation(project(":internal-testutils-truth"))
+
+    androidTestImplementation(libs.mockitoCore4, excludes.bytebuddy) // DexMaker has it"s own MockMaker
+    androidTestImplementation(libs.dexmakerMockitoInline, excludes.bytebuddy) // DexMaker has it"s own MockMaker
+}
+
+android {
+    compileSdk = 33
+    compileSdkExtension = 4
+    namespace "androidx.privacysandbox.ads.adservices"
+}
+
+androidx {
+    name = "Androidx library for Privacy Preserving APIs."
+    type = LibraryType.PUBLISHED_LIBRARY
+    publish = Publish.SNAPSHOT_AND_RELEASE
+    inceptionYear = "2022"
+    description = "This library enables integration with Privacy Preserving APIs, which are part of Privacy Sandbox on Android."
+}
diff --git a/privacysandbox/ads/ads-adservices/src/androidTest/AndroidManifest.xml b/privacysandbox/ads/ads-adservices/src/androidTest/AndroidManifest.xml
new file mode 100644
index 0000000..3f2e804
--- /dev/null
+++ b/privacysandbox/ads/ads-adservices/src/androidTest/AndroidManifest.xml
@@ -0,0 +1,18 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+  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.
+  -->
+<manifest xmlns:android="http://schemas.android.com/apk/res/android">
+</manifest>
\ No newline at end of file
diff --git a/privacysandbox/ads/ads-adservices/src/androidTest/java/androidx/privacysandbox/ads/adservices/adid/AdIdManagerTest.kt b/privacysandbox/ads/ads-adservices/src/androidTest/java/androidx/privacysandbox/ads/adservices/adid/AdIdManagerTest.kt
new file mode 100644
index 0000000..8d71955
--- /dev/null
+++ b/privacysandbox/ads/ads-adservices/src/androidTest/java/androidx/privacysandbox/ads/adservices/adid/AdIdManagerTest.kt
@@ -0,0 +1,116 @@
+/*
+ * 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.privacysandbox.ads.adservices.adid
+
+import android.content.Context
+import android.os.OutcomeReceiver
+import android.os.ext.SdkExtensions
+import androidx.annotation.RequiresExtension
+import androidx.test.core.app.ApplicationProvider
+import androidx.test.ext.junit.runners.AndroidJUnit4
+import androidx.test.filters.SdkSuppress
+import androidx.test.filters.SmallTest
+import com.google.common.truth.Truth.assertThat
+import kotlinx.coroutines.runBlocking
+import org.junit.Assert
+import org.junit.Assume.assumeTrue
+import org.junit.Before
+import org.junit.Test
+import org.junit.runner.RunWith
+import org.mockito.ArgumentMatchers.any
+import org.mockito.Mockito.doAnswer
+import org.mockito.Mockito.mock
+import org.mockito.Mockito.spy
+import org.mockito.Mockito.verify
+import org.mockito.Mockito.`when`
+import org.mockito.invocation.InvocationOnMock
+
+@SmallTest
+@SuppressWarnings("NewApi")
+@RunWith(AndroidJUnit4::class)
+@SdkSuppress(minSdkVersion = 30)
+class AdIdManagerTest {
+
+    @Before
+    fun setUp() {
+        mContext = spy(ApplicationProvider.getApplicationContext<Context>())
+    }
+
+    @Test
+    @SdkSuppress(maxSdkVersion = 33, minSdkVersion = 30)
+    fun testAdIdOlderVersions() {
+        val sdkExtVersion = SdkExtensions.getExtensionVersion(SdkExtensions.AD_SERVICES)
+        assumeTrue("maxSdkVersion = API 33 ext 3", sdkExtVersion < 4)
+        assertThat(AdIdManager.obtain(mContext)).isEqualTo(null)
+    }
+
+    @Test
+    @RequiresExtension(extension = SdkExtensions.AD_SERVICES, version = 4)
+    fun testAdIdAsync() {
+        val sdkExtVersion = SdkExtensions.getExtensionVersion(SdkExtensions.AD_SERVICES)
+
+        assumeTrue("minSdkVersion = API 33 ext 4", sdkExtVersion >= 4)
+        val adIdManager = mockAdIdManager(mContext)
+        setupResponse(adIdManager)
+        val managerCompat = AdIdManager.obtain(mContext)
+
+        // Actually invoke the compat code.
+        val result = runBlocking {
+            managerCompat!!.getAdId()
+        }
+
+        // Verify that the compat code was invoked correctly.
+        verify(adIdManager).getAdId(any(), any())
+
+        // Verify that the result of the compat call is correct.
+        verifyResponse(result)
+    }
+
+    @SdkSuppress(minSdkVersion = 30)
+    @RequiresExtension(extension = SdkExtensions.AD_SERVICES, version = 4)
+    companion object {
+        private lateinit var mContext: Context
+
+        private fun mockAdIdManager(spyContext: Context): android.adservices.adid.AdIdManager {
+            val adIdManager = mock(android.adservices.adid.AdIdManager::class.java)
+            `when`(spyContext.getSystemService(android.adservices.adid.AdIdManager::class.java))
+                .thenReturn(adIdManager)
+            return adIdManager
+        }
+
+        private fun setupResponse(adIdManager: android.adservices.adid.AdIdManager) {
+            // Set up the response that AdIdManager will return when the compat code calls it.
+            val adId = android.adservices.adid.AdId("1234", false)
+            val answer = { args: InvocationOnMock ->
+                val receiver = args.getArgument<
+                    OutcomeReceiver<android.adservices.adid.AdId, Exception>>(1)
+                receiver.onResult(adId)
+                null
+            }
+            doAnswer(answer)
+                .`when`(adIdManager).getAdId(
+                    any(),
+                    any()
+                )
+        }
+
+        private fun verifyResponse(adId: AdId) {
+            Assert.assertEquals("1234", adId.adId)
+            Assert.assertEquals(false, adId.isLimitAdTrackingEnabled)
+        }
+    }
+}
\ No newline at end of file
diff --git a/privacysandbox/ads/ads-adservices/src/androidTest/java/androidx/privacysandbox/ads/adservices/adid/AdIdTest.kt b/privacysandbox/ads/ads-adservices/src/androidTest/java/androidx/privacysandbox/ads/adservices/adid/AdIdTest.kt
new file mode 100644
index 0000000..516d3fe
--- /dev/null
+++ b/privacysandbox/ads/ads-adservices/src/androidTest/java/androidx/privacysandbox/ads/adservices/adid/AdIdTest.kt
@@ -0,0 +1,44 @@
+/*
+ * 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.privacysandbox.ads.adservices.adid
+
+import androidx.test.ext.junit.runners.AndroidJUnit4
+import androidx.test.filters.SmallTest
+import com.google.common.truth.Truth
+import org.junit.Test
+import org.junit.runner.RunWith
+
+@SmallTest
+@RunWith(AndroidJUnit4::class)
+class AdIdTest {
+    @Test
+    fun testToString() {
+        val result = "AdId: adId=1234, isLimitAdTrackingEnabled=false"
+        val adId = AdId("1234", false)
+        Truth.assertThat(adId.toString()).isEqualTo(result)
+    }
+
+    @Test
+    fun testEquals() {
+        val adId1 = AdId("1234", false)
+        val adId2 = AdId("1234", false)
+        Truth.assertThat(adId1 == adId2).isTrue()
+
+        val adId3 = AdId("1234", true)
+        Truth.assertThat(adId1 == adId3).isFalse()
+    }
+}
\ No newline at end of file
diff --git a/privacysandbox/ads/ads-adservices/src/androidTest/java/androidx/privacysandbox/ads/adservices/adselection/AdSelectionConfigTest.kt b/privacysandbox/ads/ads-adservices/src/androidTest/java/androidx/privacysandbox/ads/adservices/adselection/AdSelectionConfigTest.kt
new file mode 100644
index 0000000..61d15e4
--- /dev/null
+++ b/privacysandbox/ads/ads-adservices/src/androidTest/java/androidx/privacysandbox/ads/adservices/adselection/AdSelectionConfigTest.kt
@@ -0,0 +1,76 @@
+/*
+ * 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.privacysandbox.ads.adservices.adselection
+
+import android.net.Uri
+import androidx.privacysandbox.ads.adservices.common.AdSelectionSignals
+import androidx.privacysandbox.ads.adservices.common.AdTechIdentifier
+import androidx.test.ext.junit.runners.AndroidJUnit4
+import androidx.test.filters.SmallTest
+import com.google.common.truth.Truth
+import org.junit.Test
+import org.junit.runner.RunWith
+
+@SmallTest
+@RunWith(AndroidJUnit4::class)
+class AdSelectionConfigTest {
+    private val seller: AdTechIdentifier = AdTechIdentifier("1234")
+    private val decisionLogicUri: Uri = Uri.parse("www.abc.com")
+    private val customAudienceBuyers: List<AdTechIdentifier> = listOf(seller)
+    private val adSelectionSignals: AdSelectionSignals = AdSelectionSignals("adSelSignals")
+    private val sellerSignals: AdSelectionSignals = AdSelectionSignals("sellerSignals")
+    private val perBuyerSignals: Map<AdTechIdentifier, AdSelectionSignals> =
+        mutableMapOf(Pair(seller, sellerSignals))
+    private val trustedScoringSignalsUri: Uri = Uri.parse("www.xyz.com")
+    @Test
+    fun testToString() {
+        val result = "AdSelectionConfig: seller=$seller, decisionLogicUri='$decisionLogicUri', " +
+            "customAudienceBuyers=$customAudienceBuyers, adSelectionSignals=$adSelectionSignals, " +
+            "sellerSignals=$sellerSignals, perBuyerSignals=$perBuyerSignals, " +
+            "trustedScoringSignalsUri=$trustedScoringSignalsUri"
+        val request = AdSelectionConfig(
+            seller,
+            decisionLogicUri,
+            customAudienceBuyers,
+            adSelectionSignals,
+            sellerSignals,
+            perBuyerSignals,
+            trustedScoringSignalsUri)
+        Truth.assertThat(request.toString()).isEqualTo(result)
+    }
+
+    @Test
+    fun testEquals() {
+        val adSelectionConfig = AdSelectionConfig(
+            seller,
+            decisionLogicUri,
+            customAudienceBuyers,
+            adSelectionSignals,
+            sellerSignals,
+            perBuyerSignals,
+            trustedScoringSignalsUri)
+        var adSelectionConfig2 = AdSelectionConfig(
+            AdTechIdentifier("1234"),
+            Uri.parse("www.abc.com"),
+            customAudienceBuyers,
+            adSelectionSignals,
+            sellerSignals,
+            perBuyerSignals,
+            trustedScoringSignalsUri)
+        Truth.assertThat(adSelectionConfig == adSelectionConfig2).isTrue()
+    }
+}
\ No newline at end of file
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
new file mode 100644
index 0000000..7ce3d77
--- /dev/null
+++ b/privacysandbox/ads/ads-adservices/src/androidTest/java/androidx/privacysandbox/ads/adservices/adselection/AdSelectionManagerTest.kt
@@ -0,0 +1,233 @@
+/*
+ * 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.privacysandbox.ads.adservices.adselection
+
+import android.adservices.adselection.AdSelectionOutcome
+import android.content.Context
+import android.net.Uri
+import android.os.OutcomeReceiver
+import android.os.ext.SdkExtensions
+import androidx.annotation.RequiresExtension
+import androidx.test.core.app.ApplicationProvider
+import androidx.privacysandbox.ads.adservices.adselection.AdSelectionManager.Companion.obtain
+import androidx.privacysandbox.ads.adservices.common.AdSelectionSignals
+import androidx.privacysandbox.ads.adservices.common.AdTechIdentifier
+import androidx.test.ext.junit.runners.AndroidJUnit4
+import androidx.test.filters.SdkSuppress
+import androidx.test.filters.SmallTest
+import com.google.common.truth.Truth.assertThat
+import kotlinx.coroutines.runBlocking
+import org.junit.Assert
+import org.junit.Assume
+import org.junit.Before
+import org.junit.Test
+import org.junit.runner.RunWith
+import org.mockito.ArgumentCaptor
+import org.mockito.ArgumentMatchers.any
+import org.mockito.Mockito.doAnswer
+import org.mockito.Mockito.mock
+import org.mockito.Mockito.spy
+import org.mockito.Mockito.verify
+import org.mockito.Mockito.`when`
+import org.mockito.invocation.InvocationOnMock
+
+@SmallTest
+@SuppressWarnings("NewApi")
+@RunWith(AndroidJUnit4::class)
+@SdkSuppress(minSdkVersion = 30)
+class AdSelectionManagerTest {
+    @Before
+    fun setUp() {
+        mContext = spy(ApplicationProvider.getApplicationContext<Context>())
+    }
+
+    @Test
+    @SdkSuppress(maxSdkVersion = 33, minSdkVersion = 30)
+    fun testAdSelectionOlderVersions() {
+        val sdkExtVersion = SdkExtensions.getExtensionVersion(SdkExtensions.AD_SERVICES)
+
+        Assume.assumeTrue("maxSdkVersion = API 33 ext 3", sdkExtVersion < 4)
+        assertThat(obtain(mContext)).isEqualTo(null)
+    }
+
+    @Test
+    @RequiresExtension(extension = SdkExtensions.AD_SERVICES, version = 4)
+    fun testSelectAds() {
+        val sdkExtVersion = SdkExtensions.getExtensionVersion(SdkExtensions.AD_SERVICES)
+
+        Assume.assumeTrue("minSdkVersion = API 33 ext 4", sdkExtVersion >= 4)
+        val adSelectionManager = mockAdSelectionManager(mContext)
+        setupAdSelectionResponse(adSelectionManager)
+        val managerCompat = obtain(mContext)
+
+        // Actually invoke the compat code.
+        val result = runBlocking {
+            managerCompat!!.selectAds(adSelectionConfig)
+        }
+
+        // Verify that the compat code was invoked correctly.
+        val captor = ArgumentCaptor.forClass(
+            android.adservices.adselection.AdSelectionConfig::class.java)
+        verify(adSelectionManager).selectAds(captor.capture(), any(), any())
+
+        // Verify that the request that the compat code makes to the platform is correct.
+        verifyRequest(captor.value)
+
+        // Verify that the result of the compat call is correct.
+        verifyResponse(result)
+    }
+
+    @Test
+    @RequiresExtension(extension = SdkExtensions.AD_SERVICES, version = 4)
+    fun testReportImpression() {
+        val sdkExtVersion = SdkExtensions.getExtensionVersion(SdkExtensions.AD_SERVICES)
+
+        Assume.assumeTrue("minSdkVersion = API 33 ext 4", sdkExtVersion >= 4)
+        val adSelectionManager = mockAdSelectionManager(mContext)
+        setupAdSelectionResponse(adSelectionManager)
+        val managerCompat = obtain(mContext)
+        val reportImpressionRequest = ReportImpressionRequest(adSelectionId, adSelectionConfig)
+
+        // Actually invoke the compat code.
+        runBlocking {
+            managerCompat!!.reportImpression(reportImpressionRequest)
+        }
+
+        // Verify that the compat code was invoked correctly.
+        val captor = ArgumentCaptor.forClass(
+            android.adservices.adselection.ReportImpressionRequest::class.java)
+        verify(adSelectionManager).reportImpression(captor.capture(), any(), any())
+
+        // Verify that the request that the compat code makes to the platform is correct.
+        verifyReportImpressionRequest(captor.value)
+    }
+
+    @SdkSuppress(minSdkVersion = 30)
+    @RequiresExtension(extension = SdkExtensions.AD_SERVICES, version = 4)
+    companion object {
+        private lateinit var mContext: Context
+        private const val adSelectionId = 1234L
+        private const val adId = "1234"
+        private val seller: AdTechIdentifier = AdTechIdentifier(adId)
+        private val decisionLogicUri: Uri = Uri.parse("www.abc.com")
+        private val customAudienceBuyers: List<AdTechIdentifier> = listOf(seller)
+        private const val adSelectionSignalsStr = "adSelSignals"
+        private val adSelectionSignals: AdSelectionSignals =
+            AdSelectionSignals(adSelectionSignalsStr)
+        private const val sellerSignalsStr = "sellerSignals"
+        private val sellerSignals: AdSelectionSignals = AdSelectionSignals(sellerSignalsStr)
+        private val perBuyerSignals: Map<AdTechIdentifier, AdSelectionSignals> =
+            mutableMapOf(Pair(seller, sellerSignals))
+        private val trustedScoringSignalsUri: Uri = Uri.parse("www.xyz.com")
+        private val adSelectionConfig = AdSelectionConfig(
+            seller,
+            decisionLogicUri,
+            customAudienceBuyers,
+            adSelectionSignals,
+            sellerSignals,
+            perBuyerSignals,
+            trustedScoringSignalsUri)
+
+        // Response.
+        private val renderUri = Uri.parse("render-uri.com")
+
+        private fun mockAdSelectionManager(
+            spyContext: Context
+        ): android.adservices.adselection.AdSelectionManager {
+            val adSelectionManager =
+                mock(android.adservices.adselection.AdSelectionManager::class.java)
+            `when`(spyContext.getSystemService(
+                android.adservices.adselection.AdSelectionManager::class.java))
+                .thenReturn(adSelectionManager)
+            return adSelectionManager
+        }
+
+        private fun setupAdSelectionResponse(
+            adSelectionManager: android.adservices.adselection.AdSelectionManager
+        ) {
+            // Set up the response that AdSelectionManager will return when the compat code calls
+            // it.
+            val response = AdSelectionOutcome.Builder()
+                .setAdSelectionId(adSelectionId)
+                .setRenderUri(renderUri)
+                .build()
+            val answer = { args: InvocationOnMock ->
+                val receiver = args.getArgument<OutcomeReceiver<AdSelectionOutcome, Exception>>(2)
+                receiver.onResult(response)
+                null
+            }
+            doAnswer(answer)
+                .`when`(adSelectionManager).selectAds(
+                    any(),
+                    any(),
+                    any()
+                )
+
+            val answer2 = { args: InvocationOnMock ->
+                val receiver = args.getArgument<OutcomeReceiver<Any, Exception>>(2)
+                receiver.onResult(Object())
+                null
+            }
+            doAnswer(answer2).`when`(adSelectionManager).reportImpression(any(), any(), any())
+        }
+
+        private fun verifyRequest(request: android.adservices.adselection.AdSelectionConfig) {
+            // Set up the request that we expect the compat code to invoke.
+            val expectedRequest = getPlatformAdSelectionConfig()
+
+            Assert.assertEquals(expectedRequest, request)
+        }
+
+        private fun verifyResponse(
+            outcome: androidx.privacysandbox.ads.adservices.adselection.AdSelectionOutcome
+        ) {
+            val expectedOutcome =
+                androidx.privacysandbox.ads.adservices.adselection.AdSelectionOutcome(
+                    adSelectionId,
+                    renderUri)
+            Assert.assertEquals(expectedOutcome, outcome)
+        }
+
+        private fun getPlatformAdSelectionConfig():
+            android.adservices.adselection.AdSelectionConfig {
+            val adTechIdentifier = android.adservices.common.AdTechIdentifier.fromString(adId)
+            return android.adservices.adselection.AdSelectionConfig.Builder()
+                .setAdSelectionSignals(
+                    android.adservices.common.AdSelectionSignals.fromString(adSelectionSignalsStr))
+                .setCustomAudienceBuyers(listOf(adTechIdentifier))
+                .setDecisionLogicUri(decisionLogicUri)
+                .setPerBuyerSignals(mutableMapOf(Pair(
+                    adTechIdentifier,
+                    android.adservices.common.AdSelectionSignals.fromString(sellerSignalsStr))))
+                .setSeller(adTechIdentifier)
+                .setSellerSignals(
+                    android.adservices.common.AdSelectionSignals.fromString(sellerSignalsStr))
+                .setTrustedScoringSignalsUri(trustedScoringSignalsUri)
+                .build()
+        }
+
+        private fun verifyReportImpressionRequest(
+            request: android.adservices.adselection.ReportImpressionRequest
+        ) {
+            val expectedRequest = android.adservices.adselection.ReportImpressionRequest(
+                adSelectionId,
+                getPlatformAdSelectionConfig())
+            Assert.assertEquals(expectedRequest.adSelectionId, request.adSelectionId)
+            Assert.assertEquals(expectedRequest.adSelectionConfig, request.adSelectionConfig)
+        }
+    }
+}
\ No newline at end of file
diff --git a/privacysandbox/ads/ads-adservices/src/androidTest/java/androidx/privacysandbox/ads/adservices/adselection/AdSelectionOutcomeTest.kt b/privacysandbox/ads/ads-adservices/src/androidTest/java/androidx/privacysandbox/ads/adservices/adselection/AdSelectionOutcomeTest.kt
new file mode 100644
index 0000000..3d1bf4d
--- /dev/null
+++ b/privacysandbox/ads/ads-adservices/src/androidTest/java/androidx/privacysandbox/ads/adservices/adselection/AdSelectionOutcomeTest.kt
@@ -0,0 +1,44 @@
+/*
+ * 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.privacysandbox.ads.adservices.adselection
+
+import android.net.Uri
+import androidx.test.ext.junit.runners.AndroidJUnit4
+import androidx.test.filters.SmallTest
+import com.google.common.truth.Truth
+import org.junit.Test
+import org.junit.runner.RunWith
+
+@SmallTest
+@RunWith(AndroidJUnit4::class)
+class AdSelectionOutcomeTest {
+    private val adSelectionId = 1234L
+    private val renderUri = Uri.parse("abc.com")
+    @Test
+    fun testToString() {
+        val result = "AdSelectionOutcome: adSelectionId=$adSelectionId, renderUri=$renderUri"
+        val request = AdSelectionOutcome(adSelectionId, renderUri)
+        Truth.assertThat(request.toString()).isEqualTo(result)
+    }
+
+    @Test
+    fun testEquals() {
+        val adSelectionOutcome = AdSelectionOutcome(adSelectionId, renderUri)
+        var adSelectionOutcome2 = AdSelectionOutcome(adSelectionId, Uri.parse("abc.com"))
+        Truth.assertThat(adSelectionOutcome == adSelectionOutcome2).isTrue()
+    }
+}
diff --git a/privacysandbox/ads/ads-adservices/src/androidTest/java/androidx/privacysandbox/ads/adservices/adselection/ReportImpressionRequestTest.kt b/privacysandbox/ads/ads-adservices/src/androidTest/java/androidx/privacysandbox/ads/adservices/adselection/ReportImpressionRequestTest.kt
new file mode 100644
index 0000000..4e9dc25
--- /dev/null
+++ b/privacysandbox/ads/ads-adservices/src/androidTest/java/androidx/privacysandbox/ads/adservices/adselection/ReportImpressionRequestTest.kt
@@ -0,0 +1,71 @@
+/*
+ * 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.privacysandbox.ads.adservices.adselection
+
+import android.net.Uri
+import androidx.privacysandbox.ads.adservices.common.AdSelectionSignals
+import androidx.privacysandbox.ads.adservices.common.AdTechIdentifier
+import androidx.test.ext.junit.runners.AndroidJUnit4
+import androidx.test.filters.SmallTest
+import com.google.common.truth.Truth
+import org.junit.Test
+import org.junit.runner.RunWith
+
+@SmallTest
+@RunWith(AndroidJUnit4::class)
+class ReportImpressionRequestTest {
+    private val adSelectionId = 1234L
+    private val seller: AdTechIdentifier = AdTechIdentifier("1234")
+    private val decisionLogicUri: Uri = Uri.parse("www.abc.com")
+    private val customAudienceBuyers: List<AdTechIdentifier> = listOf(seller)
+    private val adSelectionSignals: AdSelectionSignals = AdSelectionSignals("adSelSignals")
+    private val sellerSignals: AdSelectionSignals = AdSelectionSignals("sellerSignals")
+    private val perBuyerSignals: Map<AdTechIdentifier, AdSelectionSignals> =
+        mutableMapOf(Pair(seller, sellerSignals))
+    private val trustedScoringSignalsUri: Uri = Uri.parse("www.xyz.com")
+    private val adSelectionConfig = AdSelectionConfig(
+        seller,
+        decisionLogicUri,
+        customAudienceBuyers,
+        adSelectionSignals,
+        sellerSignals,
+        perBuyerSignals,
+        trustedScoringSignalsUri)
+
+    @Test
+    fun testToString() {
+        val result = "ReportImpressionRequest: adSelectionId=$adSelectionId, " +
+            "adSelectionConfig=$adSelectionConfig"
+        val request = ReportImpressionRequest(adSelectionId, adSelectionConfig)
+        Truth.assertThat(request.toString()).isEqualTo(result)
+    }
+
+    @Test
+    fun testEquals() {
+        val reportImpressionRequest = ReportImpressionRequest(adSelectionId, adSelectionConfig)
+        var adSelectionConfig2 = AdSelectionConfig(
+            AdTechIdentifier("1234"),
+            Uri.parse("www.abc.com"),
+            customAudienceBuyers,
+            adSelectionSignals,
+            sellerSignals,
+            perBuyerSignals,
+            trustedScoringSignalsUri)
+        var reportImpressionRequest2 = ReportImpressionRequest(adSelectionId, adSelectionConfig2)
+        Truth.assertThat(reportImpressionRequest == reportImpressionRequest2).isTrue()
+    }
+}
diff --git a/privacysandbox/ads/ads-adservices/src/androidTest/java/androidx/privacysandbox/ads/adservices/appsetid/AppSetIdManagerTest.kt b/privacysandbox/ads/ads-adservices/src/androidTest/java/androidx/privacysandbox/ads/adservices/appsetid/AppSetIdManagerTest.kt
new file mode 100644
index 0000000..863e9a7
--- /dev/null
+++ b/privacysandbox/ads/ads-adservices/src/androidTest/java/androidx/privacysandbox/ads/adservices/appsetid/AppSetIdManagerTest.kt
@@ -0,0 +1,122 @@
+/*
+ * 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.privacysandbox.ads.adservices.appsetid
+
+import android.content.Context
+import android.os.OutcomeReceiver
+import android.os.ext.SdkExtensions
+import androidx.annotation.RequiresExtension
+import androidx.test.core.app.ApplicationProvider
+import androidx.test.ext.junit.runners.AndroidJUnit4
+import androidx.test.filters.SdkSuppress
+import androidx.test.filters.SmallTest
+import com.google.common.truth.Truth.assertThat
+import kotlinx.coroutines.runBlocking
+import org.junit.Assert
+import org.junit.Assume
+import org.junit.Before
+import org.junit.Test
+import org.junit.runner.RunWith
+import org.mockito.ArgumentMatchers.any
+import org.mockito.Mockito.doAnswer
+import org.mockito.Mockito.mock
+import org.mockito.Mockito.spy
+import org.mockito.Mockito.verify
+import org.mockito.Mockito.`when`
+import org.mockito.invocation.InvocationOnMock
+
+@SmallTest
+@SuppressWarnings("NewApi")
+@RunWith(AndroidJUnit4::class)
+@SdkSuppress(minSdkVersion = 30)
+class AppSetIdManagerTest {
+    @Before
+    fun setUp() {
+        mContext = spy(ApplicationProvider.getApplicationContext<Context>())
+    }
+
+    @Test
+    @SdkSuppress(maxSdkVersion = 33, minSdkVersion = 30)
+    fun testAppSetIdOlderVersions() {
+        val sdkExtVersion = SdkExtensions.getExtensionVersion(SdkExtensions.AD_SERVICES)
+
+        Assume.assumeTrue("maxSdkVersion = API 33 ext 3", sdkExtVersion < 4)
+        assertThat(AppSetIdManager.obtain(mContext)).isEqualTo(null)
+    }
+
+    @Test
+    @SuppressWarnings("NewApi")
+    @RequiresExtension(extension = SdkExtensions.AD_SERVICES, version = 4)
+    fun testAppSetIdAsync() {
+        val sdkExtVersion = SdkExtensions.getExtensionVersion(SdkExtensions.AD_SERVICES)
+
+        Assume.assumeTrue("minSdkVersion = API 33 ext 4", sdkExtVersion >= 4)
+        val appSetIdManager = mockAppSetIdManager(mContext)
+        setupResponse(appSetIdManager)
+        val managerCompat = AppSetIdManager.obtain(mContext)
+
+        // Actually invoke the compat code.
+        val result = runBlocking {
+            managerCompat!!.getAppSetId()
+        }
+
+        // Verify that the compat code was invoked correctly.
+        verify(appSetIdManager).getAppSetId(any(), any())
+
+        // Verify that the result of the compat call is correct.
+        verifyResponse(result)
+    }
+
+    @SdkSuppress(minSdkVersion = 30)
+    @RequiresExtension(extension = SdkExtensions.AD_SERVICES, version = 4)
+    companion object {
+        private lateinit var mContext: Context
+
+        private fun mockAppSetIdManager(
+            spyContext: Context
+        ): android.adservices.appsetid.AppSetIdManager {
+            val appSetIdManager = mock(android.adservices.appsetid.AppSetIdManager::class.java)
+            `when`(spyContext.getSystemService(
+                android.adservices.appsetid.AppSetIdManager::class.java))
+                .thenReturn(appSetIdManager)
+            return appSetIdManager
+        }
+
+        private fun setupResponse(appSetIdManager: android.adservices.appsetid.AppSetIdManager) {
+            // Set up the response that AdIdManager will return when the compat code calls it.
+            val appSetId = android.adservices.appsetid.AppSetId(
+                "1234",
+                android.adservices.appsetid.AppSetId.SCOPE_APP)
+            val answer = { args: InvocationOnMock ->
+                val receiver = args.getArgument<
+                    OutcomeReceiver<android.adservices.appsetid.AppSetId, Exception>>(1)
+                receiver.onResult(appSetId)
+                null
+            }
+            doAnswer(answer)
+                .`when`(appSetIdManager).getAppSetId(
+                    any(),
+                    any()
+                )
+        }
+
+        private fun verifyResponse(appSetId: AppSetId) {
+            Assert.assertEquals("1234", appSetId.id)
+            Assert.assertEquals(AppSetId.SCOPE_APP, appSetId.scope)
+        }
+    }
+}
\ No newline at end of file
diff --git a/privacysandbox/ads/ads-adservices/src/androidTest/java/androidx/privacysandbox/ads/adservices/appsetid/AppSetIdTest.kt b/privacysandbox/ads/ads-adservices/src/androidTest/java/androidx/privacysandbox/ads/adservices/appsetid/AppSetIdTest.kt
new file mode 100644
index 0000000..49aa2db
--- /dev/null
+++ b/privacysandbox/ads/ads-adservices/src/androidTest/java/androidx/privacysandbox/ads/adservices/appsetid/AppSetIdTest.kt
@@ -0,0 +1,56 @@
+/*
+ * 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.privacysandbox.ads.adservices.appsetid
+
+import androidx.test.ext.junit.runners.AndroidJUnit4
+import androidx.test.filters.SmallTest
+import androidx.testutils.assertThrows
+import com.google.common.truth.Truth
+import org.junit.Test
+import org.junit.runner.RunWith
+
+@SmallTest
+@RunWith(AndroidJUnit4::class)
+class AppSetIdTest {
+    @Test
+    fun testToString() {
+        val result = "AppSetId: id=1234, scope=SCOPE_DEVELOPER"
+        val id = AppSetId("1234", AppSetId.SCOPE_DEVELOPER)
+        Truth.assertThat(id.toString()).isEqualTo(result)
+
+        val result2 = "AppSetId: id=4321, scope=SCOPE_APP"
+        val id2 = AppSetId("4321", AppSetId.SCOPE_APP)
+        Truth.assertThat(id2.toString()).isEqualTo(result2)
+    }
+
+    @Test
+    fun testEquals() {
+        val id1 = AppSetId("1234", AppSetId.SCOPE_DEVELOPER)
+        val id2 = AppSetId("1234", AppSetId.SCOPE_DEVELOPER)
+        Truth.assertThat(id1 == id2).isTrue()
+
+        val id3 = AppSetId("1234", AppSetId.SCOPE_APP)
+        Truth.assertThat(id1 == id3).isFalse()
+    }
+
+    @Test
+    fun testScopeUndefined() {
+        assertThrows<IllegalArgumentException> {
+            AppSetId("1234", 3 /* Invalid scope */)
+        }.hasMessageThat().contains("Scope undefined.")
+    }
+}
\ No newline at end of file
diff --git a/privacysandbox/ads/ads-adservices/src/androidTest/java/androidx/privacysandbox/ads/adservices/common/AdDataTest.kt b/privacysandbox/ads/ads-adservices/src/androidTest/java/androidx/privacysandbox/ads/adservices/common/AdDataTest.kt
new file mode 100644
index 0000000..501b15f
--- /dev/null
+++ b/privacysandbox/ads/ads-adservices/src/androidTest/java/androidx/privacysandbox/ads/adservices/common/AdDataTest.kt
@@ -0,0 +1,44 @@
+/*
+ * 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.privacysandbox.ads.adservices.common
+
+import android.net.Uri
+import androidx.test.ext.junit.runners.AndroidJUnit4
+import androidx.test.filters.SmallTest
+import com.google.common.truth.Truth
+import org.junit.Test
+import org.junit.runner.RunWith
+
+@SmallTest
+@RunWith(AndroidJUnit4::class)
+class AdDataTest {
+    private val uri: Uri = Uri.parse("abc.com")
+    private val metadata = "metadata"
+    @Test
+    fun testToString() {
+        val result = "AdData: renderUri=$uri, metadata='$metadata'"
+        val request = AdData(uri, metadata)
+        Truth.assertThat(request.toString()).isEqualTo(result)
+    }
+
+    @Test
+    fun testEquals() {
+        val adData1 = AdData(uri, metadata)
+        var adData2 = AdData(Uri.parse("abc.com"), "metadata")
+        Truth.assertThat(adData1 == adData2).isTrue()
+    }
+}
\ No newline at end of file
diff --git a/privacysandbox/ads/ads-adservices/src/androidTest/java/androidx/privacysandbox/ads/adservices/common/AdSelectionSignalsTest.kt b/privacysandbox/ads/ads-adservices/src/androidTest/java/androidx/privacysandbox/ads/adservices/common/AdSelectionSignalsTest.kt
new file mode 100644
index 0000000..21cdcd13
--- /dev/null
+++ b/privacysandbox/ads/ads-adservices/src/androidTest/java/androidx/privacysandbox/ads/adservices/common/AdSelectionSignalsTest.kt
@@ -0,0 +1,43 @@
+/*
+ * 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.privacysandbox.ads.adservices.common
+
+import androidx.test.ext.junit.runners.AndroidJUnit4
+import androidx.test.filters.SmallTest
+import com.google.common.truth.Truth
+import org.junit.Test
+import org.junit.runner.RunWith
+
+@SmallTest
+@RunWith(AndroidJUnit4::class)
+class AdSelectionSignalsTest {
+    private val signals = "signals"
+
+    @Test
+    fun testToString() {
+        val result = "AdSelectionSignals: $signals"
+        val request = AdSelectionSignals(signals)
+        Truth.assertThat(request.toString()).isEqualTo(result)
+    }
+
+    @Test
+    fun testEquals() {
+        val id1 = AdSelectionSignals(signals)
+        var id2 = AdSelectionSignals("signals")
+        Truth.assertThat(id1 == id2).isTrue()
+    }
+}
\ No newline at end of file
diff --git a/privacysandbox/ads/ads-adservices/src/androidTest/java/androidx/privacysandbox/ads/adservices/common/AdTechIdentifierTest.kt b/privacysandbox/ads/ads-adservices/src/androidTest/java/androidx/privacysandbox/ads/adservices/common/AdTechIdentifierTest.kt
new file mode 100644
index 0000000..00a98bb
--- /dev/null
+++ b/privacysandbox/ads/ads-adservices/src/androidTest/java/androidx/privacysandbox/ads/adservices/common/AdTechIdentifierTest.kt
@@ -0,0 +1,43 @@
+/*
+ * 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.privacysandbox.ads.adservices.common
+
+import androidx.test.ext.junit.runners.AndroidJUnit4
+import androidx.test.filters.SmallTest
+import com.google.common.truth.Truth
+import org.junit.Test
+import org.junit.runner.RunWith
+
+@SmallTest
+@RunWith(AndroidJUnit4::class)
+class AdTechIdentifierTest {
+    private val identifier = "ad-tech-identifier"
+
+    @Test
+    fun testToString() {
+        val result = "AdTechIdentifier: $identifier"
+        val request = AdTechIdentifier(identifier)
+        Truth.assertThat(request.toString()).isEqualTo(result)
+    }
+
+    @Test
+    fun testEquals() {
+        val id1 = AdTechIdentifier(identifier)
+        var id2 = AdTechIdentifier("ad-tech-identifier")
+        Truth.assertThat(id1 == id2).isTrue()
+    }
+}
\ No newline at end of file
diff --git a/privacysandbox/ads/ads-adservices/src/androidTest/java/androidx/privacysandbox/ads/adservices/customaudience/CustomAudienceManagerTest.kt b/privacysandbox/ads/ads-adservices/src/androidTest/java/androidx/privacysandbox/ads/adservices/customaudience/CustomAudienceManagerTest.kt
new file mode 100644
index 0000000..67037d3
--- /dev/null
+++ b/privacysandbox/ads/ads-adservices/src/androidTest/java/androidx/privacysandbox/ads/adservices/customaudience/CustomAudienceManagerTest.kt
@@ -0,0 +1,233 @@
+/*
+ * 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.privacysandbox.ads.adservices.customaudience
+
+import android.adservices.customaudience.CustomAudienceManager
+import android.content.Context
+import android.net.Uri
+import android.os.OutcomeReceiver
+import android.os.ext.SdkExtensions
+import androidx.annotation.RequiresExtension
+import androidx.privacysandbox.ads.adservices.common.AdData
+import androidx.privacysandbox.ads.adservices.common.AdSelectionSignals
+import androidx.privacysandbox.ads.adservices.common.AdTechIdentifier
+import androidx.privacysandbox.ads.adservices.customaudience.CustomAudienceManager.Companion.obtain
+import androidx.test.core.app.ApplicationProvider
+import androidx.test.ext.junit.runners.AndroidJUnit4
+import androidx.test.filters.SdkSuppress
+import androidx.test.filters.SmallTest
+import com.google.common.truth.Truth
+import java.time.Instant
+import kotlinx.coroutines.runBlocking
+import org.junit.Assume
+import org.junit.Before
+import org.junit.Test
+import org.junit.runner.RunWith
+import org.mockito.ArgumentCaptor
+import org.mockito.ArgumentMatchers.any
+import org.mockito.Mockito.doAnswer
+import org.mockito.Mockito.mock
+import org.mockito.Mockito.spy
+import org.mockito.Mockito.verify
+import org.mockito.Mockito.`when`
+import org.mockito.invocation.InvocationOnMock
+
+@SmallTest
+@SuppressWarnings("NewApi")
+@RunWith(AndroidJUnit4::class)
+@SdkSuppress(minSdkVersion = 30)
+class CustomAudienceManagerTest {
+
+    @Before
+    fun setUp() {
+        mContext = spy(ApplicationProvider.getApplicationContext<Context>())
+    }
+
+    @Test
+    @SdkSuppress(maxSdkVersion = 33, minSdkVersion = 30)
+    fun testOlderVersions() {
+        val sdkExtVersion = SdkExtensions.getExtensionVersion(SdkExtensions.AD_SERVICES)
+
+        Assume.assumeTrue("maxSdkVersion = API 33 ext 3", sdkExtVersion < 4)
+        Truth.assertThat(obtain(mContext)).isEqualTo(null)
+    }
+
+    @Test
+    @RequiresExtension(extension = SdkExtensions.AD_SERVICES, version = 4)
+    fun testJoinCustomAudience() {
+        val sdkExtVersion = SdkExtensions.getExtensionVersion(SdkExtensions.AD_SERVICES)
+
+        Assume.assumeTrue("minSdkVersion = API 33 ext 4", sdkExtVersion >= 4)
+        val customAudienceManager = mockCustomAudienceManager(mContext)
+        setupResponse(customAudienceManager)
+        val managerCompat = obtain(mContext)
+
+        // Actually invoke the compat code.
+        runBlocking {
+            val customAudience = CustomAudience.Builder(buyer, name, uri, uri, ads)
+                .setActivationTime(Instant.now())
+                .setExpirationTime(Instant.now())
+                .setUserBiddingSignals(userBiddingSignals)
+                .setTrustedBiddingData(trustedBiddingSignals)
+                .build()
+            val request = JoinCustomAudienceRequest(customAudience)
+            managerCompat!!.joinCustomAudience(request)
+        }
+
+        // Verify that the compat code was invoked correctly.
+        val captor = ArgumentCaptor.forClass(
+            android.adservices.customaudience.JoinCustomAudienceRequest::class.java
+        )
+        verify(customAudienceManager).joinCustomAudience(captor.capture(), any(), any())
+
+        // Verify that the request that the compat code makes to the platform is correct.
+        verifyJoinCustomAudienceRequest(captor.value)
+    }
+
+    @Test
+    @RequiresExtension(extension = SdkExtensions.AD_SERVICES, version = 4)
+    fun testLeaveCustomAudience() {
+        val sdkExtVersion = SdkExtensions.getExtensionVersion(SdkExtensions.AD_SERVICES)
+
+        Assume.assumeTrue("minSdkVersion = API 33 ext 4", sdkExtVersion >= 4)
+        val customAudienceManager = mockCustomAudienceManager(mContext)
+        setupResponse(customAudienceManager)
+        val managerCompat = obtain(mContext)
+
+        // Actually invoke the compat code.
+        runBlocking {
+            val request = LeaveCustomAudienceRequest(buyer, name)
+            managerCompat!!.leaveCustomAudience(request)
+        }
+
+        // Verify that the compat code was invoked correctly.
+        val captor = ArgumentCaptor.forClass(
+            android.adservices.customaudience.LeaveCustomAudienceRequest::class.java
+        )
+        verify(customAudienceManager).leaveCustomAudience(captor.capture(), any(), any())
+
+        // Verify that the request that the compat code makes to the platform is correct.
+        verifyLeaveCustomAudienceRequest(captor.value)
+    }
+
+    @SdkSuppress(minSdkVersion = 30)
+    @RequiresExtension(extension = SdkExtensions.AD_SERVICES, version = 4)
+    companion object {
+        private lateinit var mContext: Context
+        private val uri: Uri = Uri.parse("abc.com")
+        private const val adtech = "1234"
+        private val buyer: AdTechIdentifier = AdTechIdentifier(adtech)
+        private const val name: String = "abc"
+        private const val signals = "signals"
+        private val userBiddingSignals: AdSelectionSignals = AdSelectionSignals(signals)
+        private val keys: List<String> = listOf("key1", "key2")
+        private val trustedBiddingSignals: TrustedBiddingData = TrustedBiddingData(uri, keys)
+        private const val metadata = "metadata"
+        private val ads: List<AdData> = listOf(AdData(uri, metadata))
+
+        private fun mockCustomAudienceManager(spyContext: Context): CustomAudienceManager {
+            val customAudienceManager = mock(CustomAudienceManager::class.java)
+            `when`(spyContext.getSystemService(CustomAudienceManager::class.java))
+                .thenReturn(customAudienceManager)
+            return customAudienceManager
+        }
+
+        private fun setupResponse(customAudienceManager: CustomAudienceManager) {
+            val answer = { args: InvocationOnMock ->
+                val receiver = args.getArgument<OutcomeReceiver<Any, Exception>>(2)
+                receiver.onResult(Object())
+                null
+            }
+            doAnswer(answer).`when`(customAudienceManager).joinCustomAudience(any(), any(), any())
+            doAnswer(answer).`when`(customAudienceManager).leaveCustomAudience(any(), any(), any())
+        }
+
+        private fun verifyJoinCustomAudienceRequest(
+            joinCustomAudienceRequest: android.adservices.customaudience.JoinCustomAudienceRequest
+        ) {
+            // Set up the request that we expect the compat code to invoke.
+            val adtechIdentifier = android.adservices.common.AdTechIdentifier.fromString(adtech)
+            val userBiddingSignals =
+                android.adservices.common.AdSelectionSignals.fromString(signals)
+            val trustedBiddingSignals =
+                android.adservices.customaudience.TrustedBiddingData.Builder()
+                .setTrustedBiddingKeys(keys)
+                .setTrustedBiddingUri(uri)
+                .build()
+            val customAudience = android.adservices.customaudience.CustomAudience.Builder()
+                .setBuyer(adtechIdentifier)
+                .setName(name)
+                .setActivationTime(Instant.now())
+                .setExpirationTime(Instant.now())
+                .setBiddingLogicUri(uri)
+                .setDailyUpdateUri(uri)
+                .setUserBiddingSignals(userBiddingSignals)
+                .setTrustedBiddingData(trustedBiddingSignals)
+                .setAds(listOf(android.adservices.common.AdData.Builder()
+                    .setRenderUri(uri)
+                    .setMetadata(metadata)
+                    .build()))
+                .build()
+
+            val expectedRequest =
+                android.adservices.customaudience.JoinCustomAudienceRequest.Builder()
+                    .setCustomAudience(customAudience)
+                    .build()
+
+            // Verify that the actual request matches the expected one.
+            Truth.assertThat(expectedRequest.customAudience.ads.size ==
+                joinCustomAudienceRequest.customAudience.ads.size).isTrue()
+            Truth.assertThat(expectedRequest.customAudience.ads[0].renderUri ==
+                joinCustomAudienceRequest.customAudience.ads[0].renderUri).isTrue()
+            Truth.assertThat(expectedRequest.customAudience.ads[0].metadata ==
+                joinCustomAudienceRequest.customAudience.ads[0].metadata).isTrue()
+            Truth.assertThat(expectedRequest.customAudience.biddingLogicUri ==
+                joinCustomAudienceRequest.customAudience.biddingLogicUri).isTrue()
+            Truth.assertThat(expectedRequest.customAudience.buyer.toString() ==
+                joinCustomAudienceRequest.customAudience.buyer.toString()).isTrue()
+            Truth.assertThat(expectedRequest.customAudience.dailyUpdateUri ==
+                joinCustomAudienceRequest.customAudience.dailyUpdateUri).isTrue()
+            Truth.assertThat(expectedRequest.customAudience.name ==
+                joinCustomAudienceRequest.customAudience.name).isTrue()
+            Truth.assertThat(trustedBiddingSignals.trustedBiddingKeys ==
+                joinCustomAudienceRequest.customAudience.trustedBiddingData!!.trustedBiddingKeys)
+                .isTrue()
+            Truth.assertThat(trustedBiddingSignals.trustedBiddingUri ==
+                joinCustomAudienceRequest.customAudience.trustedBiddingData!!.trustedBiddingUri)
+                .isTrue()
+            Truth.assertThat(
+                joinCustomAudienceRequest.customAudience.userBiddingSignals!!.toString() ==
+                signals).isTrue()
+        }
+
+        private fun verifyLeaveCustomAudienceRequest(
+            leaveCustomAudienceRequest: android.adservices.customaudience.LeaveCustomAudienceRequest
+        ) {
+            // Set up the request that we expect the compat code to invoke.
+            val adtechIdentifier = android.adservices.common.AdTechIdentifier.fromString(adtech)
+
+            val expectedRequest = android.adservices.customaudience.LeaveCustomAudienceRequest
+                .Builder()
+                .setBuyer(adtechIdentifier)
+                .setName(name)
+                .build()
+
+            // Verify that the actual request matches the expected one.
+            Truth.assertThat(expectedRequest == leaveCustomAudienceRequest).isTrue()
+        }
+    }
+}
\ No newline at end of file
diff --git a/privacysandbox/ads/ads-adservices/src/androidTest/java/androidx/privacysandbox/ads/adservices/customaudience/CustomAudienceTest.kt b/privacysandbox/ads/ads-adservices/src/androidTest/java/androidx/privacysandbox/ads/adservices/customaudience/CustomAudienceTest.kt
new file mode 100644
index 0000000..fa5c6ce
--- /dev/null
+++ b/privacysandbox/ads/ads-adservices/src/androidTest/java/androidx/privacysandbox/ads/adservices/customaudience/CustomAudienceTest.kt
@@ -0,0 +1,81 @@
+/*
+ * 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.privacysandbox.ads.adservices.customaudience
+
+import android.net.Uri
+import androidx.privacysandbox.ads.adservices.common.AdData
+import androidx.privacysandbox.ads.adservices.common.AdSelectionSignals
+import androidx.privacysandbox.ads.adservices.common.AdTechIdentifier
+import androidx.test.ext.junit.runners.AndroidJUnit4
+import androidx.test.filters.SdkSuppress
+import androidx.test.filters.SmallTest
+import com.google.common.truth.Truth
+import java.time.Instant
+import org.junit.Test
+import org.junit.runner.RunWith
+
+@SmallTest
+@RunWith(AndroidJUnit4::class)
+@SdkSuppress(minSdkVersion = 26)
+class CustomAudienceTest {
+    private val uri: Uri = Uri.parse("abc.com")
+    private val buyer: AdTechIdentifier = AdTechIdentifier("1234")
+    private val name: String = "abc"
+    private val activationTime: Instant = Instant.now()
+    private val expirationTime: Instant = Instant.now()
+    private val userBiddingSignals: AdSelectionSignals = AdSelectionSignals("signals")
+    private val keys: List<String> = listOf("key1", "key2")
+    private val trustedBiddingSignals: TrustedBiddingData = TrustedBiddingData(uri, keys)
+    private val ads: List<AdData> = listOf(AdData(uri, "metadata"))
+
+    @Test
+    fun testToStringAndEquals() {
+        val result = "CustomAudience: buyer=abc.com, activationTime=$activationTime, " +
+            "expirationTime=$expirationTime, dailyUpdateUri=abc.com, " +
+            "userBiddingSignals=AdSelectionSignals: signals, " +
+            "trustedBiddingSignals=TrustedBiddingData: trustedBiddingUri=abc.com " +
+            "trustedBiddingKeys=[key1, key2], biddingLogicUri=abc.com, " +
+            "ads=[AdData: renderUri=abc.com, metadata='metadata']"
+
+        val customAudience = CustomAudience(
+            buyer,
+            name,
+            uri,
+            uri,
+            ads,
+            activationTime,
+            expirationTime,
+            userBiddingSignals,
+            trustedBiddingSignals)
+        Truth.assertThat(customAudience.toString()).isEqualTo(result)
+
+        // Verify Builder.
+        val customAudienceBuilder2 = CustomAudience.Builder(buyer, name, uri, uri, ads)
+            .setActivationTime(activationTime)
+            .setExpirationTime(expirationTime)
+            .setUserBiddingSignals(userBiddingSignals)
+            .setTrustedBiddingData(trustedBiddingSignals)
+        Truth.assertThat(customAudienceBuilder2.build().toString()).isEqualTo(result)
+
+        // Test equality.
+        Truth.assertThat(customAudience == customAudienceBuilder2.build()).isTrue()
+
+        // Reset values of Builder.
+        customAudienceBuilder2.setName("newName")
+        Truth.assertThat(customAudience == customAudienceBuilder2.build()).isFalse()
+    }
+}
\ No newline at end of file
diff --git a/privacysandbox/ads/ads-adservices/src/androidTest/java/androidx/privacysandbox/ads/adservices/customaudience/JoinCustomAudienceRequestTest.kt b/privacysandbox/ads/ads-adservices/src/androidTest/java/androidx/privacysandbox/ads/adservices/customaudience/JoinCustomAudienceRequestTest.kt
new file mode 100644
index 0000000..7638a70
--- /dev/null
+++ b/privacysandbox/ads/ads-adservices/src/androidTest/java/androidx/privacysandbox/ads/adservices/customaudience/JoinCustomAudienceRequestTest.kt
@@ -0,0 +1,61 @@
+/*
+ * 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.privacysandbox.ads.adservices.customaudience
+
+import android.net.Uri
+import androidx.privacysandbox.ads.adservices.common.AdData
+import androidx.privacysandbox.ads.adservices.common.AdSelectionSignals
+import androidx.privacysandbox.ads.adservices.common.AdTechIdentifier
+import androidx.test.ext.junit.runners.AndroidJUnit4
+import androidx.test.filters.SdkSuppress
+import androidx.test.filters.SmallTest
+import com.google.common.truth.Truth
+import java.time.Instant
+import org.junit.Test
+import org.junit.runner.RunWith
+
+@SmallTest
+@RunWith(AndroidJUnit4::class)
+@SdkSuppress(minSdkVersion = 26)
+class JoinCustomAudienceRequestTest {
+    private val uri: Uri = Uri.parse("abc.com")
+    private val buyer: AdTechIdentifier = AdTechIdentifier("1234")
+    private val name: String = "abc"
+    private val activationTime: Instant = Instant.now()
+    private val expirationTime: Instant = Instant.now()
+    private val userBiddingSignals: AdSelectionSignals = AdSelectionSignals("signals")
+    private val keys: List<String> = listOf("key1", "key2")
+    private val trustedBiddingSignals: TrustedBiddingData = TrustedBiddingData(uri, keys)
+    private val ads: List<AdData> = listOf(AdData(uri, "metadata"))
+
+    @Test
+    fun testToString() {
+        val customAudience = CustomAudience(
+            buyer,
+            name,
+            uri,
+            uri,
+            ads,
+            activationTime,
+            expirationTime,
+            userBiddingSignals,
+            trustedBiddingSignals)
+        val result = "JoinCustomAudience: customAudience=$customAudience"
+        val joinCustomAudienceRequest = JoinCustomAudienceRequest(customAudience)
+        Truth.assertThat(joinCustomAudienceRequest.toString()).isEqualTo(result)
+    }
+}
\ No newline at end of file
diff --git a/privacysandbox/ads/ads-adservices/src/androidTest/java/androidx/privacysandbox/ads/adservices/customaudience/LeaveCustomAudienceTest.kt b/privacysandbox/ads/ads-adservices/src/androidTest/java/androidx/privacysandbox/ads/adservices/customaudience/LeaveCustomAudienceTest.kt
new file mode 100644
index 0000000..409f780
--- /dev/null
+++ b/privacysandbox/ads/ads-adservices/src/androidTest/java/androidx/privacysandbox/ads/adservices/customaudience/LeaveCustomAudienceTest.kt
@@ -0,0 +1,37 @@
+/*
+ * 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.privacysandbox.ads.adservices.customaudience
+
+import androidx.privacysandbox.ads.adservices.common.AdTechIdentifier
+import androidx.test.ext.junit.runners.AndroidJUnit4
+import androidx.test.filters.SmallTest
+import com.google.common.truth.Truth
+import org.junit.Test
+import org.junit.runner.RunWith
+
+@SmallTest
+@RunWith(AndroidJUnit4::class)
+class LeaveCustomAudienceTest {
+    private val adTechIdentifier: AdTechIdentifier = AdTechIdentifier("1234")
+    private val name = "abc"
+    @Test
+    fun testToString() {
+        val result = "LeaveCustomAudience: buyer=AdTechIdentifier: 1234, name=abc"
+        val leaveCustomAudienceRequest = LeaveCustomAudienceRequest(adTechIdentifier, name)
+        Truth.assertThat(leaveCustomAudienceRequest.toString()).isEqualTo(result)
+    }
+}
\ No newline at end of file
diff --git a/privacysandbox/ads/ads-adservices/src/androidTest/java/androidx/privacysandbox/ads/adservices/customaudience/TrustedBiddingDataTest.kt b/privacysandbox/ads/ads-adservices/src/androidTest/java/androidx/privacysandbox/ads/adservices/customaudience/TrustedBiddingDataTest.kt
new file mode 100644
index 0000000..1476dae
--- /dev/null
+++ b/privacysandbox/ads/ads-adservices/src/androidTest/java/androidx/privacysandbox/ads/adservices/customaudience/TrustedBiddingDataTest.kt
@@ -0,0 +1,37 @@
+/*
+ * 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.privacysandbox.ads.adservices.customaudience
+
+import android.net.Uri
+import androidx.test.ext.junit.runners.AndroidJUnit4
+import androidx.test.filters.SmallTest
+import com.google.common.truth.Truth
+import org.junit.Test
+import org.junit.runner.RunWith
+
+@SmallTest
+@RunWith(AndroidJUnit4::class)
+class TrustedBiddingDataTest {
+    private val uri = Uri.parse("abc.com")
+    private val keys = listOf("key1", "key2")
+    @Test
+    fun testToString() {
+        val result = "TrustedBiddingData: trustedBiddingUri=abc.com trustedBiddingKeys=[key1, key2]"
+        val trustedBiddingData = TrustedBiddingData(uri, keys)
+        Truth.assertThat(trustedBiddingData.toString()).isEqualTo(result)
+    }
+}
\ No newline at end of file
diff --git a/privacysandbox/ads/ads-adservices/src/androidTest/java/androidx/privacysandbox/ads/adservices/measurement/DeletionRequestTest.kt b/privacysandbox/ads/ads-adservices/src/androidTest/java/androidx/privacysandbox/ads/adservices/measurement/DeletionRequestTest.kt
new file mode 100644
index 0000000..c8d6395
--- /dev/null
+++ b/privacysandbox/ads/ads-adservices/src/androidTest/java/androidx/privacysandbox/ads/adservices/measurement/DeletionRequestTest.kt
@@ -0,0 +1,67 @@
+/*
+ * 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.privacysandbox.ads.adservices.measurement
+
+import android.net.Uri
+import androidx.test.ext.junit.runners.AndroidJUnit4
+import androidx.test.filters.SdkSuppress
+import androidx.test.filters.SmallTest
+import com.google.common.truth.Truth
+import java.time.Instant
+import org.junit.Test
+import org.junit.runner.RunWith
+
+@SmallTest
+@RunWith(AndroidJUnit4::class)
+@SdkSuppress(minSdkVersion = 33)
+class DeletionRequestTest {
+    @Test
+    fun testToString() {
+        val now = Instant.now()
+        val result = "DeletionRequest { DeletionMode=DELETION_MODE_ALL, " +
+            "MatchBehavior=MATCH_BEHAVIOR_DELETE, " +
+            "Start=$now, End=$now, DomainUris=[www.abc.com], OriginUris=[www.xyz.com] }"
+
+        val deletionRequest = DeletionRequest(
+            DeletionRequest.DELETION_MODE_ALL,
+            DeletionRequest.MATCH_BEHAVIOR_DELETE,
+            now,
+            now,
+            listOf(Uri.parse("www.abc.com")),
+            listOf(Uri.parse("www.xyz.com")),
+        )
+        Truth.assertThat(deletionRequest.toString()).isEqualTo(result)
+    }
+
+    @Test
+    fun testEquals() {
+        val deletionRequest1 = DeletionRequest(
+            DeletionRequest.DELETION_MODE_ALL,
+            DeletionRequest.MATCH_BEHAVIOR_DELETE,
+            Instant.MIN,
+            Instant.MAX,
+            listOf(Uri.parse("www.abc.com")),
+            listOf(Uri.parse("www.xyz.com")))
+        val deletionRequest2 = DeletionRequest.Builder(
+            deletionMode = DeletionRequest.DELETION_MODE_ALL,
+            matchBehavior = DeletionRequest.MATCH_BEHAVIOR_DELETE)
+            .setDomainUris(listOf(Uri.parse("www.abc.com")))
+            .setOriginUris(listOf(Uri.parse("www.xyz.com")))
+            .build()
+        Truth.assertThat(deletionRequest1 == deletionRequest2).isTrue()
+    }
+}
\ No newline at end of file
diff --git a/privacysandbox/ads/ads-adservices/src/androidTest/java/androidx/privacysandbox/ads/adservices/measurement/MeasurementManagerTest.kt b/privacysandbox/ads/ads-adservices/src/androidTest/java/androidx/privacysandbox/ads/adservices/measurement/MeasurementManagerTest.kt
new file mode 100644
index 0000000..0d05aa5
--- /dev/null
+++ b/privacysandbox/ads/ads-adservices/src/androidTest/java/androidx/privacysandbox/ads/adservices/measurement/MeasurementManagerTest.kt
@@ -0,0 +1,336 @@
+/*
+ * 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.privacysandbox.ads.adservices.measurement
+
+import android.adservices.measurement.MeasurementManager
+import android.content.Context
+import android.net.Uri
+import android.os.OutcomeReceiver
+import android.os.ext.SdkExtensions
+import android.view.InputEvent
+import androidx.annotation.RequiresExtension
+import androidx.privacysandbox.ads.adservices.measurement.MeasurementManager.Companion.obtain
+import androidx.test.core.app.ApplicationProvider
+import androidx.test.ext.junit.runners.AndroidJUnit4
+import androidx.test.filters.SdkSuppress
+import androidx.test.filters.SmallTest
+import com.google.common.truth.Truth.assertThat
+import java.time.Instant
+import kotlinx.coroutines.runBlocking
+import org.junit.Assume
+import org.junit.Before
+import org.junit.Test
+import org.junit.runner.RunWith
+import org.mockito.ArgumentCaptor
+import org.mockito.ArgumentMatchers.any
+import org.mockito.Mockito.doAnswer
+import org.mockito.Mockito.mock
+import org.mockito.Mockito.spy
+import org.mockito.Mockito.verify
+import org.mockito.Mockito.`when`
+import org.mockito.invocation.InvocationOnMock
+
+@SmallTest
+@SuppressWarnings("NewApi")
+@RunWith(AndroidJUnit4::class)
+@SdkSuppress(minSdkVersion = 30)
+class MeasurementManagerTest {
+
+    @Before
+    fun setUp() {
+        mContext = spy(ApplicationProvider.getApplicationContext<Context>())
+    }
+
+    @Test
+    @SdkSuppress(maxSdkVersion = 33, minSdkVersion = 30)
+    fun testMeasurementOlderVersions() {
+        val sdkExtVersion = SdkExtensions.getExtensionVersion(SdkExtensions.AD_SERVICES)
+
+        Assume.assumeTrue("maxSdkVersion = API 33 ext 3", sdkExtVersion < 4)
+        assertThat(obtain(mContext)).isEqualTo(null)
+    }
+
+    @Test
+    @RequiresExtension(extension = SdkExtensions.AD_SERVICES, version = 4)
+    fun testDeleteRegistrations() {
+        val sdkExtVersion = SdkExtensions.getExtensionVersion(SdkExtensions.AD_SERVICES)
+
+        Assume.assumeTrue("minSdkVersion = API 33 ext 4", sdkExtVersion >= 4)
+        val measurementManager = mockMeasurementManager(mContext)
+        val managerCompat = obtain(mContext)
+
+        // Set up the request.
+        val answer = { args: InvocationOnMock ->
+            val receiver = args.getArgument<OutcomeReceiver<Any, Exception>>(2)
+            receiver.onResult(Object())
+            null
+        }
+        doAnswer(answer).`when`(measurementManager).deleteRegistrations(any(), any(), any())
+
+        // Actually invoke the compat code.
+        runBlocking {
+            val request = DeletionRequest(
+                DeletionRequest.DELETION_MODE_ALL,
+                DeletionRequest.MATCH_BEHAVIOR_DELETE,
+                Instant.now(),
+                Instant.now(),
+                listOf(uri1),
+                listOf(uri1))
+
+            managerCompat!!.deleteRegistrations(request)
+        }
+
+        // Verify that the compat code was invoked correctly.
+        val captor = ArgumentCaptor.forClass(
+            android.adservices.measurement.DeletionRequest::class.java
+        )
+        verify(measurementManager).deleteRegistrations(captor.capture(), any(), any())
+
+        // Verify that the request that the compat code makes to the platform is correct.
+        verifyDeletionRequest(captor.value)
+    }
+
+    @Test
+    @RequiresExtension(extension = SdkExtensions.AD_SERVICES, version = 4)
+    fun testRegisterSource() {
+        val sdkExtVersion = SdkExtensions.getExtensionVersion(SdkExtensions.AD_SERVICES)
+
+        Assume.assumeTrue("minSdkVersion = API 33 ext 4", sdkExtVersion >= 4)
+        val inputEvent = mock(InputEvent::class.java)
+        val measurementManager = mockMeasurementManager(mContext)
+        val managerCompat = obtain(mContext)
+        val answer = { args: InvocationOnMock ->
+            val receiver = args.getArgument<OutcomeReceiver<Any, Exception>>(3)
+            receiver.onResult(Object())
+            null
+        }
+        doAnswer(answer).`when`(measurementManager).registerSource(any(), any(), any(), any())
+
+        // Actually invoke the compat code.
+        runBlocking {
+            managerCompat!!.registerSource(uri1, inputEvent)
+        }
+
+        // Verify that the compat code was invoked correctly.
+        val captor1 = ArgumentCaptor.forClass(Uri::class.java)
+        val captor2 = ArgumentCaptor.forClass(InputEvent::class.java)
+        verify(measurementManager).registerSource(
+            captor1.capture(),
+            captor2.capture(),
+            any(),
+            any())
+
+        // Verify that the request that the compat code makes to the platform is correct.
+        assertThat(captor1.value == uri1)
+        assertThat(captor2.value == inputEvent)
+    }
+
+    @Test
+    @RequiresExtension(extension = SdkExtensions.AD_SERVICES, version = 4)
+    fun testRegisterTrigger() {
+        val sdkExtVersion = SdkExtensions.getExtensionVersion(SdkExtensions.AD_SERVICES)
+
+        Assume.assumeTrue("minSdkVersion = API 33 ext 4", sdkExtVersion >= 4)
+        val measurementManager = mockMeasurementManager(mContext)
+        val managerCompat = obtain(mContext)
+        val answer = { args: InvocationOnMock ->
+            val receiver = args.getArgument<OutcomeReceiver<Any, Exception>>(2)
+            receiver.onResult(Object())
+            null
+        }
+        doAnswer(answer).`when`(measurementManager).registerTrigger(any(), any(), any())
+
+        // Actually invoke the compat code.
+        runBlocking {
+            managerCompat!!.registerTrigger(uri1)
+        }
+
+        // Verify that the compat code was invoked correctly.
+        val captor1 = ArgumentCaptor.forClass(Uri::class.java)
+        verify(measurementManager).registerTrigger(
+            captor1.capture(),
+            any(),
+            any())
+
+        // Verify that the request that the compat code makes to the platform is correct.
+        assertThat(captor1.value).isEqualTo(uri1)
+    }
+
+    @Test
+    @RequiresExtension(extension = SdkExtensions.AD_SERVICES, version = 4)
+    fun testRegisterWebSource() {
+        val sdkExtVersion = SdkExtensions.getExtensionVersion(SdkExtensions.AD_SERVICES)
+
+        Assume.assumeTrue("minSdkVersion = API 33 ext 4", sdkExtVersion >= 4)
+        val measurementManager = mockMeasurementManager(mContext)
+        val managerCompat = obtain(mContext)
+        val answer = { args: InvocationOnMock ->
+            val receiver = args.getArgument<OutcomeReceiver<Any, Exception>>(2)
+            receiver.onResult(Object())
+            null
+        }
+        doAnswer(answer).`when`(measurementManager).registerWebSource(any(), any(), any())
+
+        val request = WebSourceRegistrationRequest.Builder(
+            listOf(WebSourceParams(uri1, false)), uri1)
+            .setAppDestination(uri1)
+            .build()
+
+        // Actually invoke the compat code.
+        runBlocking {
+            managerCompat!!.registerWebSource(request)
+        }
+
+        // Verify that the compat code was invoked correctly.
+        val captor1 = ArgumentCaptor.forClass(
+            android.adservices.measurement.WebSourceRegistrationRequest::class.java)
+        verify(measurementManager).registerWebSource(
+            captor1.capture(),
+            any(),
+            any())
+
+        // Verify that the request that the compat code makes to the platform is correct.
+        val actualRequest = captor1.value
+        assertThat(actualRequest.topOriginUri == uri1)
+        assertThat(actualRequest.sourceParams.size == 1)
+        assertThat(actualRequest.sourceParams[0].registrationUri == uri1)
+        assertThat(!actualRequest.sourceParams[0].isDebugKeyAllowed)
+    }
+
+    @Test
+    @RequiresExtension(extension = SdkExtensions.AD_SERVICES, version = 4)
+    fun testRegisterWebTrigger() {
+        val sdkExtVersion = SdkExtensions.getExtensionVersion(SdkExtensions.AD_SERVICES)
+
+        Assume.assumeTrue("minSdkVersion = API 33 ext 4", sdkExtVersion >= 4)
+        val measurementManager = mockMeasurementManager(mContext)
+        val managerCompat = obtain(mContext)
+        val answer = { args: InvocationOnMock ->
+            val receiver = args.getArgument<OutcomeReceiver<Any, Exception>>(2)
+            receiver.onResult(Object())
+            null
+        }
+        doAnswer(answer).`when`(measurementManager).registerWebTrigger(any(), any(), any())
+
+        val request = WebTriggerRegistrationRequest(
+            listOf(WebTriggerParams(uri1, false)), uri2)
+
+        // Actually invoke the compat code.
+        runBlocking {
+            managerCompat!!.registerWebTrigger(request)
+        }
+
+        // Verify that the compat code was invoked correctly.
+        val captor1 = ArgumentCaptor.forClass(
+            android.adservices.measurement.WebTriggerRegistrationRequest::class.java)
+        verify(measurementManager).registerWebTrigger(
+            captor1.capture(),
+            any(),
+            any())
+
+        // Verify that the request that the compat code makes to the platform is correct.
+        val actualRequest = captor1.value
+        assertThat(actualRequest.destination).isEqualTo(uri2)
+        assertThat(actualRequest.triggerParams.size == 1)
+        assertThat(actualRequest.triggerParams[0].registrationUri == uri1)
+        assertThat(!actualRequest.triggerParams[0].isDebugKeyAllowed)
+    }
+
+    @Test
+    @RequiresExtension(extension = SdkExtensions.AD_SERVICES, version = 4)
+    fun testMeasurementApiStatus() {
+        val sdkExtVersion = SdkExtensions.getExtensionVersion(SdkExtensions.AD_SERVICES)
+
+        Assume.assumeTrue("minSdkVersion = API 33 ext 4", sdkExtVersion >= 4)
+        val measurementManager = mockMeasurementManager(mContext)
+        val managerCompat = obtain(mContext)
+        val state = MeasurementManager.MEASUREMENT_API_STATE_ENABLED
+        val answer = { args: InvocationOnMock ->
+            val receiver = args.getArgument<OutcomeReceiver<Int, Exception>>(1)
+            receiver.onResult(state)
+            null
+        }
+        doAnswer(answer).`when`(measurementManager).getMeasurementApiStatus(any(), any())
+
+        // Actually invoke the compat code.
+        val actualResult = runBlocking {
+            managerCompat!!.getMeasurementApiStatus()
+        }
+
+        // Verify that the compat code was invoked correctly.
+        verify(measurementManager).getMeasurementApiStatus(any(), any())
+
+        // Verify that the request that the compat code makes to the platform is correct.
+        assertThat(actualResult == state)
+    }
+
+    @Test
+    @RequiresExtension(extension = SdkExtensions.AD_SERVICES, version = 4)
+    fun testMeasurementApiStatusUnknown() {
+        val sdkExtVersion = SdkExtensions.getExtensionVersion(SdkExtensions.AD_SERVICES)
+
+        Assume.assumeTrue("minSdkVersion = API 33 ext 4", sdkExtVersion >= 4)
+        val measurementManager = mockMeasurementManager(mContext)
+        val managerCompat = obtain(mContext)
+        val answer = { args: InvocationOnMock ->
+            val receiver = args.getArgument<OutcomeReceiver<Int, Exception>>(1)
+            receiver.onResult(5 /* Greater than values returned in SdkExtensions.AD_SERVICES = 4 */)
+            null
+        }
+        doAnswer(answer).`when`(measurementManager).getMeasurementApiStatus(any(), any())
+
+        // Actually invoke the compat code.
+        val actualResult = runBlocking {
+            managerCompat!!.getMeasurementApiStatus()
+        }
+
+        // Verify that the compat code was invoked correctly.
+        verify(measurementManager).getMeasurementApiStatus(any(), any())
+
+        // Verify that the request that the compat code makes to the platform is correct.
+        // Since the compat code does not know the returned state, it sets it to UNKNOWN.
+        assertThat(actualResult == 5)
+    }
+
+    @SdkSuppress(minSdkVersion = 30)
+    @RequiresExtension(extension = SdkExtensions.AD_SERVICES, version = 4)
+    companion object {
+
+        private val uri1: Uri = Uri.parse("www.abc.com")
+        private val uri2: Uri = Uri.parse("http://www.xyz.com")
+
+        private lateinit var mContext: Context
+
+        private fun mockMeasurementManager(spyContext: Context): MeasurementManager {
+            val measurementManager = mock(MeasurementManager::class.java)
+            `when`(spyContext.getSystemService(MeasurementManager::class.java))
+                .thenReturn(measurementManager)
+            return measurementManager
+        }
+
+        private fun verifyDeletionRequest(request: android.adservices.measurement.DeletionRequest) {
+            // Set up the request that we expect the compat code to invoke.
+            val expectedRequest = android.adservices.measurement.DeletionRequest.Builder()
+                .setDomainUris(listOf(uri1))
+                .setOriginUris(listOf(uri1))
+                .build()
+
+            assertThat(HashSet(request.domainUris) == HashSet(expectedRequest.domainUris))
+            assertThat(HashSet(request.originUris) == HashSet(expectedRequest.originUris))
+        }
+    }
+}
\ No newline at end of file
diff --git a/privacysandbox/ads/ads-adservices/src/androidTest/java/androidx/privacysandbox/ads/adservices/measurement/WebSourceParamsTest.kt b/privacysandbox/ads/ads-adservices/src/androidTest/java/androidx/privacysandbox/ads/adservices/measurement/WebSourceParamsTest.kt
new file mode 100644
index 0000000..4850355
--- /dev/null
+++ b/privacysandbox/ads/ads-adservices/src/androidTest/java/androidx/privacysandbox/ads/adservices/measurement/WebSourceParamsTest.kt
@@ -0,0 +1,48 @@
+/*
+ * 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.privacysandbox.ads.adservices.measurement
+
+import android.net.Uri
+import androidx.test.ext.junit.runners.AndroidJUnit4
+import androidx.test.filters.SdkSuppress
+import androidx.test.filters.SmallTest
+import com.google.common.truth.Truth
+import org.junit.Test
+import org.junit.runner.RunWith
+
+@SmallTest
+@RunWith(AndroidJUnit4::class)
+@SdkSuppress(minSdkVersion = 33)
+class WebSourceParamsTest {
+    @Test
+    fun testToString() {
+        val result = "WebSourceParams { RegistrationUri=www.abc.com, DebugKeyAllowed=false }"
+
+        val request = WebSourceParams(Uri.parse("www.abc.com"), false)
+        Truth.assertThat(request.toString()).isEqualTo(result)
+    }
+
+    @Test
+    fun testEquals() {
+        val request1 = WebSourceParams(Uri.parse("www.abc.com"), false)
+        val request2 = WebSourceParams(Uri.parse("www.abc.com"), false)
+        val request3 = WebSourceParams(Uri.parse("https://abc.com"), false)
+
+        Truth.assertThat(request1 == request2).isTrue()
+        Truth.assertThat(request1 == request3).isFalse()
+    }
+}
\ No newline at end of file
diff --git a/privacysandbox/ads/ads-adservices/src/androidTest/java/androidx/privacysandbox/ads/adservices/measurement/WebSourceRegistrationRequestTest.kt b/privacysandbox/ads/ads-adservices/src/androidTest/java/androidx/privacysandbox/ads/adservices/measurement/WebSourceRegistrationRequestTest.kt
new file mode 100644
index 0000000..a404850
--- /dev/null
+++ b/privacysandbox/ads/ads-adservices/src/androidTest/java/androidx/privacysandbox/ads/adservices/measurement/WebSourceRegistrationRequestTest.kt
@@ -0,0 +1,79 @@
+/*
+ * 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.privacysandbox.ads.adservices.measurement
+
+import android.net.Uri
+import androidx.test.ext.junit.runners.AndroidJUnit4
+import androidx.test.filters.SdkSuppress
+import androidx.test.filters.SmallTest
+import com.google.common.truth.Truth
+import org.junit.Test
+import org.junit.runner.RunWith
+
+@SmallTest
+@RunWith(AndroidJUnit4::class)
+@SdkSuppress(minSdkVersion = 33)
+class WebSourceRegistrationRequestTest {
+    @Test
+    fun testToString() {
+        val result = "WebSourceRegistrationRequest { WebSourceParams=" +
+            "[[WebSourceParams { RegistrationUri=www.abc.com, DebugKeyAllowed=false }]], " +
+            "TopOriginUri=www.abc.com, InputEvent=null, AppDestination=null, WebDestination=null," +
+            " VerifiedDestination=null }"
+
+        val uri = Uri.parse("www.abc.com")
+        val params = listOf(WebSourceParams(uri, false))
+        val request = WebSourceRegistrationRequest.Builder(params, uri).build()
+        Truth.assertThat(request.toString()).isEqualTo(result)
+
+        val result2 = "WebSourceRegistrationRequest { WebSourceParams=[[WebSourceParams " +
+            "{ RegistrationUri=www.abc.com, DebugKeyAllowed=false }]], TopOriginUri=www.abc.com, " +
+            "InputEvent=null, AppDestination=www.abc.com, WebDestination=www.abc.com, " +
+            "VerifiedDestination=www.abc.com }"
+
+        val params2 = listOf(WebSourceParams(uri, false))
+        val request2 = WebSourceRegistrationRequest.Builder(params2, uri)
+            .setWebDestination(uri)
+            .setAppDestination(uri)
+            .setVerifiedDestination(uri)
+            .build()
+        Truth.assertThat(request2.toString()).isEqualTo(result2)
+    }
+
+    @Test
+    fun testEquals() {
+        val uri = Uri.parse("www.abc.com")
+
+        val params = listOf(WebSourceParams(uri, false))
+        val request1 = WebSourceRegistrationRequest.Builder(params, uri)
+            .setWebDestination(uri)
+            .setAppDestination(uri)
+            .setVerifiedDestination(uri)
+            .build()
+        val request2 = WebSourceRegistrationRequest(
+            params,
+            uri,
+            null,
+            uri,
+            uri,
+            uri)
+        val request3 = WebSourceRegistrationRequest.Builder(params, uri).build()
+
+        Truth.assertThat(request1 == request2).isTrue()
+        Truth.assertThat(request1 != request3).isTrue()
+    }
+}
\ No newline at end of file
diff --git a/privacysandbox/ads/ads-adservices/src/androidTest/java/androidx/privacysandbox/ads/adservices/measurement/WebTriggerParamsTest.kt b/privacysandbox/ads/ads-adservices/src/androidTest/java/androidx/privacysandbox/ads/adservices/measurement/WebTriggerParamsTest.kt
new file mode 100644
index 0000000..677e163
--- /dev/null
+++ b/privacysandbox/ads/ads-adservices/src/androidTest/java/androidx/privacysandbox/ads/adservices/measurement/WebTriggerParamsTest.kt
@@ -0,0 +1,48 @@
+/*
+ * 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.privacysandbox.ads.adservices.measurement
+
+import android.net.Uri
+import androidx.test.ext.junit.runners.AndroidJUnit4
+import androidx.test.filters.SdkSuppress
+import androidx.test.filters.SmallTest
+import com.google.common.truth.Truth
+import org.junit.Test
+import org.junit.runner.RunWith
+
+@SmallTest
+@RunWith(AndroidJUnit4::class)
+@SdkSuppress(minSdkVersion = 33)
+class WebTriggerParamsTest {
+    @Test
+    fun testToString() {
+        val result = "WebTriggerParams { RegistrationUri=www.abc.com, DebugKeyAllowed=false }"
+
+        val request = WebTriggerParams(Uri.parse("www.abc.com"), false)
+        Truth.assertThat(request.toString()).isEqualTo(result)
+    }
+
+    @Test
+    fun testEquals() {
+        val request1 = WebTriggerParams(Uri.parse("www.abc.com"), false)
+        val request2 = WebTriggerParams(Uri.parse("www.abc.com"), false)
+        val request3 = WebTriggerParams(Uri.parse("https://abc.com"), false)
+
+        Truth.assertThat(request1 == request2).isTrue()
+        Truth.assertThat(request1 == request3).isFalse()
+    }
+}
\ No newline at end of file
diff --git a/privacysandbox/ads/ads-adservices/src/androidTest/java/androidx/privacysandbox/ads/adservices/measurement/WebTriggerRegistrationRequestTest.kt b/privacysandbox/ads/ads-adservices/src/androidTest/java/androidx/privacysandbox/ads/adservices/measurement/WebTriggerRegistrationRequestTest.kt
new file mode 100644
index 0000000..2f64489
--- /dev/null
+++ b/privacysandbox/ads/ads-adservices/src/androidTest/java/androidx/privacysandbox/ads/adservices/measurement/WebTriggerRegistrationRequestTest.kt
@@ -0,0 +1,56 @@
+/*
+ * 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.privacysandbox.ads.adservices.measurement
+
+import android.net.Uri
+import androidx.test.ext.junit.runners.AndroidJUnit4
+import androidx.test.filters.SdkSuppress
+import androidx.test.filters.SmallTest
+import com.google.common.truth.Truth
+import org.junit.Test
+import org.junit.runner.RunWith
+
+@SmallTest
+@RunWith(AndroidJUnit4::class)
+@SdkSuppress(minSdkVersion = 33)
+class WebTriggerRegistrationRequestTest {
+    @Test
+    fun testToString() {
+        val result = "WebTriggerRegistrationRequest { WebTriggerParams=[WebTriggerParams " +
+            "{ RegistrationUri=www.abc.com, DebugKeyAllowed=false }], Destination=www.abc.com"
+
+        val uri = Uri.parse("www.abc.com")
+        val params = listOf(WebTriggerParams(uri, false))
+        val request = WebTriggerRegistrationRequest(params, uri)
+        Truth.assertThat(request.toString()).isEqualTo(result)
+    }
+
+    @Test
+    fun testEquals() {
+        val uri = Uri.parse("www.abc.com")
+
+        val params = listOf(WebTriggerParams(uri, false))
+        val request1 = WebTriggerRegistrationRequest(params, uri)
+        val request2 = WebTriggerRegistrationRequest(params, uri)
+        val request3 = WebTriggerRegistrationRequest(
+            params,
+            Uri.parse("https://abc.com"))
+
+        Truth.assertThat(request1 == request2).isTrue()
+        Truth.assertThat(request1 != request3).isTrue()
+    }
+}
\ No newline at end of file
diff --git a/privacysandbox/ads/ads-adservices/src/androidTest/java/androidx/privacysandbox/ads/adservices/topics/GetTopicsRequestTest.kt b/privacysandbox/ads/ads-adservices/src/androidTest/java/androidx/privacysandbox/ads/adservices/topics/GetTopicsRequestTest.kt
new file mode 100644
index 0000000..a04b48d
--- /dev/null
+++ b/privacysandbox/ads/ads-adservices/src/androidTest/java/androidx/privacysandbox/ads/adservices/topics/GetTopicsRequestTest.kt
@@ -0,0 +1,44 @@
+/*
+ * 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.privacysandbox.ads.adservices.topics
+
+import androidx.test.ext.junit.runners.AndroidJUnit4
+import androidx.test.filters.SmallTest
+import com.google.common.truth.Truth
+import org.junit.Test
+import org.junit.runner.RunWith
+
+@SmallTest
+@RunWith(AndroidJUnit4::class)
+class GetTopicsRequestTest {
+    @Test
+    fun testToString() {
+        val result = "GetTopicsRequest: sdkName=sdk1, shouldRecordObservation=false"
+        val request = GetTopicsRequest("sdk1", false)
+        Truth.assertThat(request.toString()).isEqualTo(result)
+
+        // Verify Builder.
+        val request2 = GetTopicsRequest.Builder()
+            .setSdkName("sdk1")
+            .setShouldRecordObservation(false)
+            .build()
+        Truth.assertThat(request.toString()).isEqualTo(result)
+
+        // Verify equality.
+        Truth.assertThat(request == request2).isTrue()
+    }
+}
\ No newline at end of file
diff --git a/privacysandbox/ads/ads-adservices/src/androidTest/java/androidx/privacysandbox/ads/adservices/topics/GetTopicsResponseTest.kt b/privacysandbox/ads/ads-adservices/src/androidTest/java/androidx/privacysandbox/ads/adservices/topics/GetTopicsResponseTest.kt
new file mode 100644
index 0000000..1078022
--- /dev/null
+++ b/privacysandbox/ads/ads-adservices/src/androidTest/java/androidx/privacysandbox/ads/adservices/topics/GetTopicsResponseTest.kt
@@ -0,0 +1,48 @@
+/*
+ * 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.privacysandbox.ads.adservices.topics
+
+import androidx.test.ext.junit.runners.AndroidJUnit4
+import androidx.test.filters.SmallTest
+import com.google.common.truth.Truth
+import org.junit.Test
+import org.junit.runner.RunWith
+
+@SmallTest
+@RunWith(AndroidJUnit4::class)
+class GetTopicsResponseTest {
+    @Test
+    fun testToString() {
+        val topicsString = "Topic { TaxonomyVersion=1, ModelVersion=10, TopicCode=100 }, " +
+            "Topic { TaxonomyVersion=2, ModelVersion=20, TopicCode=200 }"
+        val result = "Topics=[$topicsString]"
+
+        val topic1 = Topic(1, 10, 100)
+        var topic2 = Topic(2, 20, 200)
+        val response1 = GetTopicsResponse(listOf(topic1, topic2))
+        Truth.assertThat(response1.toString()).isEqualTo(result)
+    }
+
+    @Test
+    fun testEquals() {
+        val topic1 = Topic(1, 10, 100)
+        var topic2 = Topic(2, 20, 200)
+        val response1 = GetTopicsResponse(listOf(topic1, topic2))
+        val response2 = GetTopicsResponse(listOf(topic2, topic1))
+        Truth.assertThat(response1 == response2).isTrue()
+    }
+}
\ No newline at end of file
diff --git a/privacysandbox/ads/ads-adservices/src/androidTest/java/androidx/privacysandbox/ads/adservices/topics/TopicTest.kt b/privacysandbox/ads/ads-adservices/src/androidTest/java/androidx/privacysandbox/ads/adservices/topics/TopicTest.kt
new file mode 100644
index 0000000..baac9ff
--- /dev/null
+++ b/privacysandbox/ads/ads-adservices/src/androidTest/java/androidx/privacysandbox/ads/adservices/topics/TopicTest.kt
@@ -0,0 +1,41 @@
+/*
+ * 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.privacysandbox.ads.adservices.topics
+
+import androidx.test.ext.junit.runners.AndroidJUnit4
+import androidx.test.filters.SmallTest
+import com.google.common.truth.Truth
+import org.junit.Test
+import org.junit.runner.RunWith
+
+@SmallTest
+@RunWith(AndroidJUnit4::class)
+class TopicTest {
+    @Test
+    fun testToString() {
+        val result = "Topic { TaxonomyVersion=1, ModelVersion=10, TopicCode=100 }"
+        val topic = Topic(/* taxonomyVersion= */ 1, /* modelVersion= */ 10, /* topicId= */ 100)
+        Truth.assertThat(topic.toString()).isEqualTo(result)
+    }
+
+    @Test
+    fun testEquals() {
+        val topic1 = Topic(/* taxonomyVersion= */ 1, /* modelVersion= */ 10, /* topicId= */ 100)
+        val topic2 = Topic(/* taxonomyVersion= */ 1, /* modelVersion= */ 10, /* topicId= */ 100)
+        Truth.assertThat(topic1 == topic2).isTrue()
+    }
+}
\ No newline at end of file
diff --git a/privacysandbox/ads/ads-adservices/src/androidTest/java/androidx/privacysandbox/ads/adservices/topics/TopicsManagerTest.kt b/privacysandbox/ads/ads-adservices/src/androidTest/java/androidx/privacysandbox/ads/adservices/topics/TopicsManagerTest.kt
new file mode 100644
index 0000000..3619d38
--- /dev/null
+++ b/privacysandbox/ads/ads-adservices/src/androidTest/java/androidx/privacysandbox/ads/adservices/topics/TopicsManagerTest.kt
@@ -0,0 +1,177 @@
+/*
+ * 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.privacysandbox.ads.adservices.topics
+
+import android.adservices.topics.Topic
+import android.adservices.topics.TopicsManager
+import android.content.Context
+import android.os.OutcomeReceiver
+import android.os.ext.SdkExtensions
+import androidx.annotation.RequiresExtension
+import androidx.privacysandbox.ads.adservices.topics.TopicsManager.Companion.obtain
+import androidx.test.core.app.ApplicationProvider
+import androidx.test.ext.junit.runners.AndroidJUnit4
+import androidx.test.filters.SdkSuppress
+import androidx.test.filters.SmallTest
+import androidx.testutils.assertThrows
+import com.google.common.truth.Truth.assertThat
+import java.lang.IllegalArgumentException
+import kotlinx.coroutines.runBlocking
+import org.junit.Assert
+import org.junit.Assume
+import org.junit.Before
+import org.junit.Test
+import org.junit.runner.RunWith
+import org.mockito.ArgumentCaptor
+import org.mockito.ArgumentMatchers.any
+import org.mockito.Mockito.doAnswer
+import org.mockito.Mockito.mock
+import org.mockito.Mockito.spy
+import org.mockito.Mockito.verify
+import org.mockito.Mockito.`when`
+import org.mockito.invocation.InvocationOnMock
+
+@SmallTest
+@SuppressWarnings("NewApi")
+@RunWith(AndroidJUnit4::class)
+@SdkSuppress(minSdkVersion = 30)
+class TopicsManagerTest {
+
+    @Before
+    fun setUp() {
+        mContext = spy(ApplicationProvider.getApplicationContext<Context>())
+    }
+
+    @Test
+    @SdkSuppress(maxSdkVersion = 33, minSdkVersion = 30)
+    fun testTopicsOlderVersions() {
+        val sdkExtVersion = SdkExtensions.getExtensionVersion(SdkExtensions.AD_SERVICES)
+
+        Assume.assumeTrue("maxSdkVersion = API 33 ext 3", sdkExtVersion < 4)
+        assertThat(obtain(mContext)).isEqualTo(null)
+    }
+
+    @Test
+    @SuppressWarnings("NewApi")
+    @RequiresExtension(extension = SdkExtensions.AD_SERVICES, version = 4)
+    fun testTopicsAsync() {
+        val sdkExtVersion = SdkExtensions.getExtensionVersion(SdkExtensions.AD_SERVICES)
+
+        Assume.assumeTrue("minSdkVersion = API 33 ext 4", sdkExtVersion >= 4)
+        val topicsManager = mockTopicsManager(mContext)
+        setupTopicsResponse(topicsManager)
+        val managerCompat = obtain(mContext)
+
+        // Actually invoke the compat code.
+        val result = runBlocking {
+            val request = GetTopicsRequest.Builder()
+                .setSdkName(mSdkName)
+                .setShouldRecordObservation(true)
+                .build()
+
+            managerCompat!!.getTopics(request)
+        }
+
+        // Verify that the compat code was invoked correctly.
+        val captor = ArgumentCaptor.forClass(android.adservices.topics.GetTopicsRequest::class.java)
+        verify(topicsManager).getTopics(captor.capture(), any(), any())
+
+        // Verify that the request that the compat code makes to the platform is correct.
+        verifyRequest(captor.value)
+
+        // Verify that the result of the compat call is correct.
+        verifyResponse(result)
+    }
+
+    @Test
+    @RequiresExtension(extension = SdkExtensions.AD_SERVICES, version = 4)
+    fun testTopicsAsyncPreviewNotSupported() {
+        val sdkExtVersion = SdkExtensions.getExtensionVersion(SdkExtensions.AD_SERVICES)
+
+        Assume.assumeTrue("minSdkVersion = API 33 ext 4", sdkExtVersion >= 4)
+        val topicsManager = mockTopicsManager(mContext)
+        setupTopicsResponse(topicsManager)
+        val managerCompat = obtain(mContext)
+
+        val request = GetTopicsRequest.Builder()
+            .setSdkName(mSdkName)
+            .setShouldRecordObservation(false)
+            .build()
+
+        // Actually invoke the compat code.
+        assertThrows<IllegalArgumentException> {
+            runBlocking {
+                managerCompat!!.getTopics(request)
+            }
+        }.hasMessageThat().contains("shouldRecordObservation not supported yet.")
+    }
+
+    @SdkSuppress(minSdkVersion = 30)
+    @RequiresExtension(extension = SdkExtensions.AD_SERVICES, version = 4)
+    companion object {
+        private lateinit var mContext: Context
+        private val mSdkName: String = "sdk1"
+
+        private fun mockTopicsManager(spyContext: Context): TopicsManager {
+            val topicsManager = mock(TopicsManager::class.java)
+            `when`(spyContext.getSystemService(TopicsManager::class.java))
+                .thenReturn(topicsManager)
+            return topicsManager
+        }
+
+        private fun setupTopicsResponse(topicsManager: TopicsManager) {
+            // Set up the response that TopicsManager will return when the compat code calls it.
+            val topic1 = Topic(1, 1, 1)
+            val topic2 = Topic(2, 2, 2)
+            val topics = listOf(topic1, topic2)
+            val response = android.adservices.topics.GetTopicsResponse.Builder(topics).build()
+            val answer = { args: InvocationOnMock ->
+                val receiver = args.getArgument<
+                    OutcomeReceiver<android.adservices.topics.GetTopicsResponse, Exception>>(2)
+                receiver.onResult(response)
+                null
+            }
+            doAnswer(answer)
+                .`when`(topicsManager).getTopics(
+                    any(),
+                    any(),
+                    any()
+                )
+        }
+
+        private fun verifyRequest(topicsRequest: android.adservices.topics.GetTopicsRequest) {
+            // Set up the request that we expect the compat code to invoke.
+            val expectedRequest = android.adservices.topics.GetTopicsRequest.Builder()
+                .setAdsSdkName(mSdkName)
+                .build()
+
+            Assert.assertEquals(expectedRequest.adsSdkName, topicsRequest.adsSdkName)
+        }
+
+        private fun verifyResponse(getTopicsResponse: GetTopicsResponse) {
+            Assert.assertEquals(2, getTopicsResponse.topics.size)
+            val topic1 = getTopicsResponse.topics[0]
+            val topic2 = getTopicsResponse.topics[1]
+            Assert.assertEquals(1, topic1.topicId)
+            Assert.assertEquals(1, topic1.modelVersion)
+            Assert.assertEquals(1, topic1.taxonomyVersion)
+            Assert.assertEquals(2, topic2.topicId)
+            Assert.assertEquals(2, topic2.modelVersion)
+            Assert.assertEquals(2, topic2.taxonomyVersion)
+        }
+    }
+}
\ No newline at end of file
diff --git a/privacysandbox/ads/ads-adservices/src/main/java/androidx/privacysandbox/ads/adservices/adid/AdId.kt b/privacysandbox/ads/ads-adservices/src/main/java/androidx/privacysandbox/ads/adservices/adid/AdId.kt
new file mode 100644
index 0000000..ad9ba22
--- /dev/null
+++ b/privacysandbox/ads/ads-adservices/src/main/java/androidx/privacysandbox/ads/adservices/adid/AdId.kt
@@ -0,0 +1,50 @@
+/*
+ * 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.privacysandbox.ads.adservices.adid
+
+/**
+ * A unique, user-resettable, device-wide, per-profile ID for advertising as returned by the
+ * [AdIdManager#getAdId()] API.
+ *
+ * Ad networks may use {@code AdId} to monetize for Interest Based Advertising (IBA), i.e.
+ * targeting and remarketing ads. The user may limit availability of this identifier.
+ *
+ * @param adId The advertising ID.
+ * @param isLimitAdTrackingEnabled the limit ad tracking enabled setting.
+ */
+class AdId internal constructor(
+    val adId: String,
+    val isLimitAdTrackingEnabled: Boolean = false
+) {
+
+    override fun equals(other: Any?): Boolean {
+        if (this === other) return true
+        if (other !is AdId) return false
+        return this.adId == other.adId &&
+            this.isLimitAdTrackingEnabled == other.isLimitAdTrackingEnabled
+    }
+
+    override fun hashCode(): Int {
+        var hash = adId.hashCode()
+        hash = 31 * hash + isLimitAdTrackingEnabled.hashCode()
+        return hash
+    }
+
+    override fun toString(): String {
+        return "AdId: adId=$adId, isLimitAdTrackingEnabled=$isLimitAdTrackingEnabled"
+    }
+}
\ No newline at end of file
diff --git a/privacysandbox/ads/ads-adservices/src/main/java/androidx/privacysandbox/ads/adservices/adid/AdIdManager.kt b/privacysandbox/ads/ads-adservices/src/main/java/androidx/privacysandbox/ads/adservices/adid/AdIdManager.kt
new file mode 100644
index 0000000..ef41c4d
--- /dev/null
+++ b/privacysandbox/ads/ads-adservices/src/main/java/androidx/privacysandbox/ads/adservices/adid/AdIdManager.kt
@@ -0,0 +1,97 @@
+/*
+ * 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.privacysandbox.ads.adservices.adid
+
+import android.adservices.common.AdServicesPermissions
+import android.annotation.SuppressLint
+import android.content.Context
+import android.os.ext.SdkExtensions
+import android.os.LimitExceededException
+import androidx.annotation.DoNotInline
+import androidx.annotation.RequiresExtension
+import androidx.annotation.RequiresPermission
+import androidx.core.os.asOutcomeReceiver
+import androidx.privacysandbox.ads.adservices.internal.AdServicesInfo
+import kotlinx.coroutines.suspendCancellableCoroutine
+
+/**
+ * AdId Manager provides APIs for app and ad-SDKs to access advertising ID. The advertising ID is a
+ * unique, per-device, user-resettable ID for advertising. It gives users better controls and
+ * provides developers with a simple, standard system to continue to monetize their apps via
+ * personalized ads (formerly known as interest-based ads).
+ */
+abstract class AdIdManager internal constructor() {
+    /**
+     * Return the AdId.
+     *
+     * @throws SecurityException if caller is not authorized to call this API.
+     * @throws IllegalStateException if this API is not available.
+     * @throws LimitExceededException if rate limit was reached.
+     */
+    @RequiresPermission(AdServicesPermissions.ACCESS_ADSERVICES_AD_ID)
+    abstract suspend fun getAdId(): AdId
+
+    @SuppressLint("ClassVerificationFailure", "NewApi")
+    @RequiresExtension(extension = SdkExtensions.AD_SERVICES, version = 4)
+    private class Api33Ext4Impl(
+        private val mAdIdManager: android.adservices.adid.AdIdManager
+    ) : AdIdManager() {
+        constructor(context: Context) : this(
+            context.getSystemService<android.adservices.adid.AdIdManager>(
+                android.adservices.adid.AdIdManager::class.java
+            )
+        )
+
+        @DoNotInline
+        @RequiresPermission(AdServicesPermissions.ACCESS_ADSERVICES_AD_ID)
+        override suspend fun getAdId(): AdId {
+            return convertResponse(getAdIdAsyncInternal())
+        }
+
+        @RequiresPermission(AdServicesPermissions.ACCESS_ADSERVICES_AD_ID)
+        private suspend fun
+            getAdIdAsyncInternal(): android.adservices.adid.AdId = suspendCancellableCoroutine {
+                continuation ->
+            mAdIdManager.getAdId(
+                Runnable::run,
+                continuation.asOutcomeReceiver()
+            )
+        }
+
+        private fun convertResponse(response: android.adservices.adid.AdId): AdId {
+            return AdId(response.adId, response.isLimitAdTrackingEnabled)
+        }
+    }
+
+    companion object {
+        /**
+         *  Creates [AdIdManager].
+         *
+         *  @return AdIdManager object.
+         */
+        @JvmStatic
+        @SuppressLint("NewApi", "ClassVerificationFailure")
+        fun obtain(context: Context): AdIdManager? {
+            return if (AdServicesInfo.version() >= 4) {
+                Api33Ext4Impl(context)
+            } else {
+                // TODO(b/261770989): Extend this to older versions.
+                null
+            }
+        }
+    }
+}
\ No newline at end of file
diff --git a/privacysandbox/ads/ads-adservices/src/main/java/androidx/privacysandbox/ads/adservices/adselection/AdSelectionConfig.kt b/privacysandbox/ads/ads-adservices/src/main/java/androidx/privacysandbox/ads/adservices/adselection/AdSelectionConfig.kt
new file mode 100644
index 0000000..839b734
--- /dev/null
+++ b/privacysandbox/ads/ads-adservices/src/main/java/androidx/privacysandbox/ads/adservices/adselection/AdSelectionConfig.kt
@@ -0,0 +1,87 @@
+/*
+ * 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.privacysandbox.ads.adservices.adselection
+
+import android.net.Uri
+import androidx.privacysandbox.ads.adservices.common.AdSelectionSignals
+import androidx.privacysandbox.ads.adservices.common.AdTechIdentifier
+
+/**
+ * Contains the configuration of the ad selection process.
+ *
+ * Instances of this class are created by SDKs to be provided as arguments to the
+ * [AdSelectionManager#selectAds] and [AdSelectionManager#reportImpression] methods in
+ * [AdSelectionManager].
+ *
+ * @param seller AdTechIdentifier of the seller, for example "www.example-ssp.com".
+ * @param decisionLogicUri the URI used to retrieve the JS code containing the seller/SSP scoreAd
+ *     function used during the ad selection and reporting processes.
+ * @param customAudienceBuyers a list of custom audience buyers allowed by the SSP to participate
+ *     in the ad selection process.
+ * @param adSelectionSignals signals given to the participating buyers in the ad selection and
+ *     reporting processes.
+ * @param sellerSignals represents any information that the SSP used in the ad
+ *     scoring process to tweak the results of the ad selection process (e.g. brand safety
+ *     checks, excluded contextual ads).
+ * @param perBuyerSignals any information that each buyer would provide during ad selection to
+ *     participants (such as bid floor, ad selection type, etc.)
+ * @param trustedScoringSignalsUri URI endpoint of sell-side trusted signal from which creative
+ *     specific realtime information can be fetched from.
+ */
+class AdSelectionConfig public constructor(
+    val seller: AdTechIdentifier,
+    val decisionLogicUri: Uri,
+    val customAudienceBuyers: List<AdTechIdentifier>,
+    val adSelectionSignals: AdSelectionSignals,
+    val sellerSignals: AdSelectionSignals,
+    val perBuyerSignals: Map<AdTechIdentifier, AdSelectionSignals>,
+    val trustedScoringSignalsUri: Uri
+) {
+
+    /** Checks whether two [AdSelectionConfig] objects contain the same information.  */
+    override fun equals(other: Any?): Boolean {
+        if (this === other) return true
+        if (other !is AdSelectionConfig) return false
+        return this.seller == other.seller &&
+            this.decisionLogicUri == other.decisionLogicUri &&
+            this.customAudienceBuyers == other.customAudienceBuyers &&
+            this.adSelectionSignals == other.adSelectionSignals &&
+            this.sellerSignals == other.sellerSignals &&
+            this.perBuyerSignals == other.perBuyerSignals &&
+            this.trustedScoringSignalsUri == other.trustedScoringSignalsUri
+    }
+
+    /** Returns the hash of the [AdSelectionConfig] object's data.  */
+    override fun hashCode(): Int {
+        var hash = seller.hashCode()
+        hash = 31 * hash + decisionLogicUri.hashCode()
+        hash = 31 * hash + customAudienceBuyers.hashCode()
+        hash = 31 * hash + adSelectionSignals.hashCode()
+        hash = 31 * hash + sellerSignals.hashCode()
+        hash = 31 * hash + perBuyerSignals.hashCode()
+        hash = 31 * hash + trustedScoringSignalsUri.hashCode()
+        return hash
+    }
+
+    /** Overrides the toString method.  */
+    override fun toString(): String {
+        return "AdSelectionConfig: seller=$seller, decisionLogicUri='$decisionLogicUri', " +
+            "customAudienceBuyers=$customAudienceBuyers, adSelectionSignals=$adSelectionSignals, " +
+            "sellerSignals=$sellerSignals, perBuyerSignals=$perBuyerSignals, " +
+            "trustedScoringSignalsUri=$trustedScoringSignalsUri"
+    }
+}
\ No newline at end of file
diff --git a/privacysandbox/ads/ads-adservices/src/main/java/androidx/privacysandbox/ads/adservices/adselection/AdSelectionManager.kt b/privacysandbox/ads/ads-adservices/src/main/java/androidx/privacysandbox/ads/adservices/adselection/AdSelectionManager.kt
new file mode 100644
index 0000000..454a7cb
--- /dev/null
+++ b/privacysandbox/ads/ads-adservices/src/main/java/androidx/privacysandbox/ads/adservices/adselection/AdSelectionManager.kt
@@ -0,0 +1,198 @@
+/*
+ * 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.privacysandbox.ads.adservices.adselection
+
+import android.adservices.common.AdServicesPermissions
+import android.annotation.SuppressLint
+import android.content.Context
+import android.os.LimitExceededException
+import android.os.TransactionTooLargeException
+import android.os.ext.SdkExtensions
+import androidx.annotation.DoNotInline
+import androidx.annotation.RequiresExtension
+import androidx.annotation.RequiresPermission
+import androidx.core.os.asOutcomeReceiver
+import androidx.privacysandbox.ads.adservices.common.AdSelectionSignals
+import androidx.privacysandbox.ads.adservices.internal.AdServicesInfo
+import androidx.privacysandbox.ads.adservices.common.AdTechIdentifier
+import java.util.concurrent.TimeoutException
+import kotlinx.coroutines.suspendCancellableCoroutine
+
+/**
+ * AdSelection Manager provides APIs for app and ad-SDKs to run ad selection processes as well
+ * as report impressions.
+ */
+abstract class AdSelectionManager internal constructor() {
+    /**
+     * Runs the ad selection process on device to select a remarketing ad for the caller
+     * application.
+     *
+     * @param adSelectionConfig the config The input {@code adSelectionConfig} is provided by the
+     * Ads SDK and the [AdSelectionConfig] object is transferred via a Binder call. For this
+     * reason, the total size of these objects is bound to the Android IPC limitations. Failures to
+     * transfer the [AdSelectionConfig] will throws an [TransactionTooLargeException].
+     *
+     * The output is passed by the receiver, which either returns an [AdSelectionOutcome]
+     * for a successful run, or an [Exception] includes the type of the exception thrown and
+     * the corresponding error message.
+     *
+     * If the [IllegalArgumentException] is thrown, it is caused by invalid input argument
+     * the API received to run the ad selection.
+     *
+     * If the [IllegalStateException] is thrown with error message "Failure of AdSelection
+     * services.", it is caused by an internal failure of the ad selection service.
+     *
+     * If the [TimeoutException] is thrown, it is caused when a timeout is encountered
+     * during bidding, scoring, or overall selection process to find winning Ad.
+     *
+     * If the [LimitExceededException] is thrown, it is caused when the calling package
+     * exceeds the allowed rate limits and is throttled.
+     */
+    @RequiresPermission(AdServicesPermissions.ACCESS_ADSERVICES_CUSTOM_AUDIENCE)
+    abstract suspend fun selectAds(adSelectionConfig: AdSelectionConfig): AdSelectionOutcome
+
+    /**
+     * Report the given impression. The [ReportImpressionRequest] is provided by the Ads SDK.
+     * The receiver either returns a {@code void} for a successful run, or an [Exception]
+     * indicates the error.
+     *
+     * @param reportImpressionRequest the request for reporting impression.
+     */
+    @RequiresPermission(AdServicesPermissions.ACCESS_ADSERVICES_CUSTOM_AUDIENCE)
+    abstract suspend fun reportImpression(reportImpressionRequest: ReportImpressionRequest)
+
+    @SuppressLint("NewApi", "ClassVerificationFailure")
+    @RequiresExtension(extension = SdkExtensions.AD_SERVICES, version = 4)
+    private class Api33Ext4Impl(
+        private val mAdSelectionManager: android.adservices.adselection.AdSelectionManager
+    ) : AdSelectionManager() {
+        constructor(context: Context) : this(
+            context.getSystemService<android.adservices.adselection.AdSelectionManager>(
+                android.adservices.adselection.AdSelectionManager::class.java
+            )
+        )
+
+        @DoNotInline
+        @RequiresPermission(AdServicesPermissions.ACCESS_ADSERVICES_CUSTOM_AUDIENCE)
+        override suspend fun selectAds(adSelectionConfig: AdSelectionConfig): AdSelectionOutcome {
+            return convertResponse(selectAdsInternal(convertAdSelectionConfig(adSelectionConfig)))
+        }
+
+        @RequiresPermission(AdServicesPermissions.ACCESS_ADSERVICES_CUSTOM_AUDIENCE)
+        private suspend fun selectAdsInternal(
+            adSelectionConfig: android.adservices.adselection.AdSelectionConfig
+        ): android.adservices.adselection.AdSelectionOutcome = suspendCancellableCoroutine { cont
+            ->
+            mAdSelectionManager.selectAds(
+                adSelectionConfig,
+                Runnable::run,
+                cont.asOutcomeReceiver()
+            )
+        }
+
+        private fun convertAdSelectionConfig(
+            request: AdSelectionConfig
+        ): android.adservices.adselection.AdSelectionConfig {
+            return android.adservices.adselection.AdSelectionConfig.Builder()
+                .setAdSelectionSignals(convertAdSelectionSignals(request.adSelectionSignals))
+                .setCustomAudienceBuyers(convertBuyers(request.customAudienceBuyers))
+                .setDecisionLogicUri(request.decisionLogicUri)
+                .setSeller(android.adservices.common.AdTechIdentifier.fromString(
+                    request.seller.identifier))
+                .setPerBuyerSignals(convertPerBuyerSignals(request.perBuyerSignals))
+                .setSellerSignals(convertAdSelectionSignals(request.sellerSignals))
+                .setTrustedScoringSignalsUri(request.trustedScoringSignalsUri)
+                .build()
+        }
+
+        private fun convertAdSelectionSignals(
+            request: AdSelectionSignals
+        ): android.adservices.common.AdSelectionSignals {
+            return android.adservices.common.AdSelectionSignals.fromString(request.signals)
+        }
+
+        private fun convertBuyers(
+            buyers: List<AdTechIdentifier>
+        ): MutableList<android.adservices.common.AdTechIdentifier> {
+            var ids = mutableListOf<android.adservices.common.AdTechIdentifier>()
+            for (buyer in buyers) {
+                ids.add(android.adservices.common.AdTechIdentifier.fromString(buyer.identifier))
+            }
+            return ids
+        }
+
+        private fun convertPerBuyerSignals(
+            request: Map<AdTechIdentifier, AdSelectionSignals>
+        ): Map<android.adservices.common.AdTechIdentifier,
+            android.adservices.common.AdSelectionSignals?> {
+            var map = HashMap<android.adservices.common.AdTechIdentifier,
+                android.adservices.common.AdSelectionSignals?>()
+            for (key in request.keys) {
+                val id = android.adservices.common.AdTechIdentifier.fromString(key.identifier)
+                val value = if (request[key] != null) convertAdSelectionSignals(request[key]!!)
+                    else null
+                map[id] = value
+            }
+            return map
+        }
+
+        private fun convertResponse(
+            response: android.adservices.adselection.AdSelectionOutcome
+        ): AdSelectionOutcome {
+            return AdSelectionOutcome(response.adSelectionId, response.renderUri)
+        }
+
+        @DoNotInline
+        @RequiresPermission(AdServicesPermissions.ACCESS_ADSERVICES_CUSTOM_AUDIENCE)
+        override suspend fun reportImpression(reportImpressionRequest: ReportImpressionRequest) {
+            suspendCancellableCoroutine<Any> { continuation ->
+                mAdSelectionManager.reportImpression(
+                    convertReportImpressionRequest(reportImpressionRequest),
+                    Runnable::run,
+                    continuation.asOutcomeReceiver()
+                )
+            }
+        }
+
+        private fun convertReportImpressionRequest(
+            request: ReportImpressionRequest
+        ): android.adservices.adselection.ReportImpressionRequest {
+            return android.adservices.adselection.ReportImpressionRequest(
+                request.adSelectionId,
+                convertAdSelectionConfig(request.adSelectionConfig)
+            )
+        }
+    }
+
+    companion object {
+        /**
+         *  Creates [AdSelectionManager].
+         *
+         *  @return AdSelectionManager object. If the device is running an incompatible
+         *  build, the value returned is null.
+         */
+        @JvmStatic
+        @SuppressLint("NewApi", "ClassVerificationFailure")
+        fun obtain(context: Context): AdSelectionManager? {
+            return if (AdServicesInfo.version() >= 4) {
+                Api33Ext4Impl(context)
+            } else {
+                null
+            }
+        }
+    }
+}
diff --git a/privacysandbox/ads/ads-adservices/src/main/java/androidx/privacysandbox/ads/adservices/adselection/AdSelectionOutcome.kt b/privacysandbox/ads/ads-adservices/src/main/java/androidx/privacysandbox/ads/adservices/adselection/AdSelectionOutcome.kt
new file mode 100644
index 0000000..9286e12
--- /dev/null
+++ b/privacysandbox/ads/ads-adservices/src/main/java/androidx/privacysandbox/ads/adservices/adselection/AdSelectionOutcome.kt
@@ -0,0 +1,54 @@
+/*
+ * Copyright 2022 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.privacysandbox.ads.adservices.adselection
+
+import android.net.Uri
+
+/**
+ * This class represents  input to the [AdSelectionManager#selectAds] in the
+ * [AdSelectionManager]. This field is populated in the case of a successful
+ * [AdSelectionManager#selectAds] call.
+ *
+ * @param adSelectionId An ID unique only to a device user that identifies a successful ad
+ *     selection.
+ * @param renderUri A render URL for the winning ad.
+ */
+class AdSelectionOutcome public constructor(
+    val adSelectionId: Long,
+    val renderUri: Uri
+) {
+
+    /** Checks whether two [AdSelectionOutcome] objects contain the same information.  */
+    override fun equals(other: Any?): Boolean {
+        if (this === other) return true
+        if (other !is AdSelectionOutcome) return false
+        return this.adSelectionId == other.adSelectionId &&
+            this.renderUri == other.renderUri
+    }
+
+    /** Returns the hash of the [AdSelectionOutcome] object's data.  */
+    override fun hashCode(): Int {
+        var hash = adSelectionId.hashCode()
+        hash = 31 * hash + renderUri.hashCode()
+        return hash
+    }
+
+    /** Overrides the toString method.  */
+    override fun toString(): String {
+        return "AdSelectionOutcome: adSelectionId=$adSelectionId, renderUri=$renderUri"
+    }
+}
diff --git a/privacysandbox/ads/ads-adservices/src/main/java/androidx/privacysandbox/ads/adservices/adselection/ReportImpressionRequest.kt b/privacysandbox/ads/ads-adservices/src/main/java/androidx/privacysandbox/ads/adservices/adselection/ReportImpressionRequest.kt
new file mode 100644
index 0000000..3740b5d
--- /dev/null
+++ b/privacysandbox/ads/ads-adservices/src/main/java/androidx/privacysandbox/ads/adservices/adselection/ReportImpressionRequest.kt
@@ -0,0 +1,52 @@
+/*
+ * 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.privacysandbox.ads.adservices.adselection
+
+/**
+ * Represent input parameters to the reportImpression API.
+ *
+ * @param adSelectionId An ID unique only to a device user that identifies a successful ad
+ *     selection.
+ * @param adSelectionConfig The same configuration used in the selectAds() call identified by the
+ *      provided ad selection ID.
+ */
+class ReportImpressionRequest public constructor(
+    val adSelectionId: Long,
+    val adSelectionConfig: AdSelectionConfig
+) {
+
+    /** Checks whether two [ReportImpressionRequest] objects contain the same information.  */
+    override fun equals(other: Any?): Boolean {
+        if (this === other) return true
+        if (other !is ReportImpressionRequest) return false
+        return this.adSelectionId == other.adSelectionId &&
+            this.adSelectionConfig == other.adSelectionConfig
+    }
+
+    /** Returns the hash of the [ReportImpressionRequest] object's data.  */
+    override fun hashCode(): Int {
+        var hash = adSelectionId.hashCode()
+        hash = 31 * hash + adSelectionConfig.hashCode()
+        return hash
+    }
+
+    /** Overrides the toString method.  */
+    override fun toString(): String {
+        return "ReportImpressionRequest: adSelectionId=$adSelectionId, " +
+            "adSelectionConfig=$adSelectionConfig"
+    }
+}
\ No newline at end of file
diff --git a/privacysandbox/ads/ads-adservices/src/main/java/androidx/privacysandbox/ads/adservices/appsetid/AppSetId.kt b/privacysandbox/ads/ads-adservices/src/main/java/androidx/privacysandbox/ads/adservices/appsetid/AppSetId.kt
new file mode 100644
index 0000000..516ba9e
--- /dev/null
+++ b/privacysandbox/ads/ads-adservices/src/main/java/androidx/privacysandbox/ads/adservices/appsetid/AppSetId.kt
@@ -0,0 +1,70 @@
+/*
+ * 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.privacysandbox.ads.adservices.appsetid
+
+/**
+ * A unique, per-device, per developer-account user-resettable ID for non-monetizing advertising
+ * use cases.
+ *
+ * Represents the appSetID and scope of this appSetId from the
+ * [AppSetIdManager#getAppSetId()] API. The scope of the ID can be per app or per developer account
+ * associated with the user. AppSetId is used for analytics, spam detection, frequency capping and
+ * fraud prevention use cases, on a given device, that one may need to correlate usage or actions
+ * across a set of apps owned by an organization.
+ *
+ * @param id The appSetID.
+ * @param scope The scope of the ID. Can be AppSetId.SCOPE_APP or AppSetId.SCOPE_DEVELOPER.
+ */
+class AppSetId public constructor(
+    val id: String,
+    val scope: Int
+) {
+    init {
+        require(scope == SCOPE_APP || scope == SCOPE_DEVELOPER) { "Scope undefined." }
+    }
+
+    override fun equals(other: Any?): Boolean {
+        if (this === other) return true
+        if (other !is AppSetId) return false
+        return this.id == other.id &&
+            this.scope == other.scope
+    }
+
+    override fun hashCode(): Int {
+        var hash = id.hashCode()
+        hash = 31 * hash + scope.hashCode()
+        return hash
+    }
+
+    override fun toString(): String {
+        var scopeStr = if (scope == 1) "SCOPE_APP" else "SCOPE_DEVELOPER"
+        return "AppSetId: id=$id, scope=$scopeStr"
+    }
+
+    companion object {
+        /**
+         * The appSetId is scoped to an app. All apps on a device will have a different appSetId.
+         */
+        public const val SCOPE_APP = 1
+
+        /**
+         * The appSetId is scoped to a developer account on an app store. All apps from the same
+         * developer on a device will have the same developer scoped appSetId.
+         */
+        public const val SCOPE_DEVELOPER = 2
+    }
+}
\ No newline at end of file
diff --git a/privacysandbox/ads/ads-adservices/src/main/java/androidx/privacysandbox/ads/adservices/appsetid/AppSetIdManager.kt b/privacysandbox/ads/ads-adservices/src/main/java/androidx/privacysandbox/ads/adservices/appsetid/AppSetIdManager.kt
new file mode 100644
index 0000000..844de9a
--- /dev/null
+++ b/privacysandbox/ads/ads-adservices/src/main/java/androidx/privacysandbox/ads/adservices/appsetid/AppSetIdManager.kt
@@ -0,0 +1,93 @@
+/*
+ * 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.privacysandbox.ads.adservices.appsetid
+
+import android.annotation.SuppressLint
+import android.content.Context
+import android.os.LimitExceededException
+import android.os.ext.SdkExtensions
+import androidx.annotation.DoNotInline
+import androidx.annotation.RequiresExtension
+import androidx.core.os.asOutcomeReceiver
+import androidx.privacysandbox.ads.adservices.internal.AdServicesInfo
+import kotlinx.coroutines.suspendCancellableCoroutine
+
+/**
+ * AppSetIdManager provides APIs for app and ad-SDKs to access appSetId for non-monetizing purpose.
+ */
+abstract class AppSetIdManager internal constructor() {
+    /**
+     * Retrieve the AppSetId.
+     *
+     * @throws [SecurityException] if caller is not authorized to call this API.
+     * @throws [IllegalStateException] if this API is not available.
+     * @throws [LimitExceededException] if rate limit was reached.
+     */
+    abstract suspend fun getAppSetId(): AppSetId
+
+    @SuppressLint("ClassVerificationFailure", "NewApi")
+    @RequiresExtension(extension = SdkExtensions.AD_SERVICES, version = 4)
+    private class Api33Ext4Impl(
+        private val mAppSetIdManager: android.adservices.appsetid.AppSetIdManager
+    ) : AppSetIdManager() {
+        constructor(context: Context) : this(
+            context.getSystemService<android.adservices.appsetid.AppSetIdManager>(
+                android.adservices.appsetid.AppSetIdManager::class.java
+            )
+        )
+
+        @DoNotInline
+        override suspend fun getAppSetId(): AppSetId {
+            return convertResponse(getAppSetIdAsyncInternal())
+        }
+
+        private suspend fun getAppSetIdAsyncInternal(): android.adservices.appsetid.AppSetId =
+            suspendCancellableCoroutine {
+                    continuation ->
+                mAppSetIdManager.getAppSetId(
+                    Runnable::run,
+                    continuation.asOutcomeReceiver()
+                )
+            }
+
+        private fun convertResponse(response: android.adservices.appsetid.AppSetId): AppSetId {
+            if (response.scope == android.adservices.appsetid.AppSetId.SCOPE_APP) {
+                return AppSetId(response.id, AppSetId.SCOPE_APP)
+            }
+            return AppSetId(response.id, AppSetId.SCOPE_DEVELOPER)
+        }
+    }
+
+    companion object {
+
+        /**
+         *  Creates [AppSetIdManager].
+         *
+         *  @return AppSetIdManager object.
+         */
+        @JvmStatic
+        @SuppressLint("NewApi", "ClassVerificationFailure")
+        fun obtain(context: Context): AppSetIdManager? {
+            return if (AdServicesInfo.version() >= 4) {
+                Api33Ext4Impl(context)
+            } else {
+                // TODO(b/261770989): Extend this to older versions.
+                null
+            }
+        }
+    }
+}
\ No newline at end of file
diff --git a/privacysandbox/ads/ads-adservices/src/main/java/androidx/privacysandbox/ads/adservices/common/AdData.kt b/privacysandbox/ads/ads-adservices/src/main/java/androidx/privacysandbox/ads/adservices/common/AdData.kt
new file mode 100644
index 0000000..ec458ba
--- /dev/null
+++ b/privacysandbox/ads/ads-adservices/src/main/java/androidx/privacysandbox/ads/adservices/common/AdData.kt
@@ -0,0 +1,50 @@
+/*
+ * 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.privacysandbox.ads.adservices.common
+
+import android.net.Uri
+
+/**
+ * Represents data specific to an ad that is necessary for ad selection and rendering.
+ * @param renderUri a URI pointing to the ad's rendering assets
+ * @param metadata buyer ad metadata represented as a JSON string
+ */
+class AdData public constructor(
+    val renderUri: Uri,
+    val metadata: String
+    ) {
+
+    /** Checks whether two [AdData] objects contain the same information.  */
+    override fun equals(other: Any?): Boolean {
+        if (this === other) return true
+        if (other !is AdData) return false
+        return this.renderUri == other.renderUri &&
+            this.metadata == other.metadata
+    }
+
+    /** Returns the hash of the [AdData] object's data.  */
+    override fun hashCode(): Int {
+        var hash = renderUri.hashCode()
+        hash = 31 * hash + metadata.hashCode()
+        return hash
+    }
+
+    /** Overrides the toString method.  */
+    override fun toString(): String {
+        return "AdData: renderUri=$renderUri, metadata='$metadata'"
+    }
+}
\ No newline at end of file
diff --git a/privacysandbox/ads/ads-adservices/src/main/java/androidx/privacysandbox/ads/adservices/common/AdSelectionSignals.kt b/privacysandbox/ads/ads-adservices/src/main/java/androidx/privacysandbox/ads/adservices/common/AdSelectionSignals.kt
new file mode 100644
index 0000000..5495ae5
--- /dev/null
+++ b/privacysandbox/ads/ads-adservices/src/main/java/androidx/privacysandbox/ads/adservices/common/AdSelectionSignals.kt
@@ -0,0 +1,62 @@
+/*
+ * 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.privacysandbox.ads.adservices.common
+
+/**
+ * This class holds JSON that will be passed into a JavaScript function during ad selection. Its
+ * contents are not used by <a
+ * href="https://developer.android.com/design-for-safety/privacy-sandbox/fledge">FLEDGE</a> platform
+ * code, but are merely validated and then passed to the appropriate JavaScript ad selection
+ * function.
+ * @param signals Any valid JSON string to create the AdSelectionSignals with.
+ */
+class AdSelectionSignals public constructor(val signals: String) {
+    /**
+     * Compares this AdSelectionSignals to the specified object. The result is true if and only if
+     * the argument is not null and the signals property of the two objects are equal.
+     * Note that this method will not perform any JSON normalization so two AdSelectionSignals
+     * objects with the same JSON could be not equal if the String representations of the objects
+     * was not equal.
+     *
+     * @param other The object to compare this AdSelectionSignals against
+     * @return true if the given object represents an AdSelectionSignals equivalent to this
+     * AdSelectionSignals, false otherwise
+     */
+    override fun equals(other: Any?): Boolean {
+        if (this === other) return true
+        if (other !is AdSelectionSignals) return false
+        return this.signals == other.signals
+    }
+
+    /**
+     * Returns a hash code corresponding to the string representation of this class obtained by
+     * calling [.toString]. Note that this method will not perform any JSON normalization so
+     * two AdSelectionSignals objects with the same JSON could have different hash codes if the
+     * underlying string representation was different.
+     *
+     * @return a hash code value for this object.
+     */
+    override fun hashCode(): Int {
+        return signals.hashCode()
+    }
+
+    /** @return The String form of the JSON wrapped by this class.
+     */
+    override fun toString(): String {
+        return "AdSelectionSignals: $signals"
+    }
+}
diff --git a/privacysandbox/ads/ads-adservices/src/main/java/androidx/privacysandbox/ads/adservices/common/AdTechIdentifier.kt b/privacysandbox/ads/ads-adservices/src/main/java/androidx/privacysandbox/ads/adservices/common/AdTechIdentifier.kt
new file mode 100644
index 0000000..775e14e
--- /dev/null
+++ b/privacysandbox/ads/ads-adservices/src/main/java/androidx/privacysandbox/ads/adservices/common/AdTechIdentifier.kt
@@ -0,0 +1,60 @@
+/*
+ * 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.privacysandbox.ads.adservices.common
+
+/**
+ * An Identifier representing an ad buyer or seller.
+ *
+ * @param identifier The identifier.
+ */
+class AdTechIdentifier public constructor(val identifier: String) {
+
+    /**
+     * Compares this AdTechIdentifier to the specified object. The result is true if and only if
+     * the argument is not null and the identifier property of the two objects are equal.
+     * Note that this method will not perform any eTLD+1 normalization so two AdTechIdentifier
+     * objects with the same eTLD+1 could be not equal if the String representations of the objects
+     * was not equal.
+     *
+     * @param other The object to compare this AdTechIdentifier against
+     * @return true if the given object represents an AdTechIdentifier equivalent to this
+     * AdTechIdentifier, false otherwise
+     */
+    override fun equals(other: Any?): Boolean {
+        if (this === other) return true
+        if (other !is AdTechIdentifier) return false
+        return this.identifier == other.identifier
+    }
+
+    /**
+     * Returns a hash code corresponding to the string representation of this class obtained by
+     * calling [.toString]. Note that this method will not perform any eTLD+1 normalization
+     * so two AdTechIdentifier objects with the same eTLD+1 could have different hash codes if the
+     * underlying string representation was different.
+     *
+     * @return a hash code value for this object.
+     */
+    override fun hashCode(): Int {
+        return identifier.hashCode()
+    }
+
+    /** @return The identifier in String form.
+     */
+    override fun toString(): String {
+        return "AdTechIdentifier: $identifier"
+    }
+}
diff --git a/privacysandbox/ads/ads-adservices/src/main/java/androidx/privacysandbox/ads/adservices/customaudience/CustomAudience.kt b/privacysandbox/ads/ads-adservices/src/main/java/androidx/privacysandbox/ads/adservices/customaudience/CustomAudience.kt
new file mode 100644
index 0000000..61e5ff7
--- /dev/null
+++ b/privacysandbox/ads/ads-adservices/src/main/java/androidx/privacysandbox/ads/adservices/customaudience/CustomAudience.kt
@@ -0,0 +1,261 @@
+/*
+ * 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.privacysandbox.ads.adservices.customaudience
+
+import android.net.Uri
+import androidx.privacysandbox.ads.adservices.common.AdData
+import androidx.privacysandbox.ads.adservices.common.AdSelectionSignals
+import androidx.privacysandbox.ads.adservices.common.AdTechIdentifier
+import java.time.Instant
+
+/**
+ * Represents the information necessary for a custom audience to participate in ad selection.
+ *
+ * A custom audience is an abstract grouping of users with similar demonstrated interests. This
+ * class is a collection of some data stored on a device that is necessary to serve advertisements
+ * targeting a single custom audience.
+ *
+ * @param buyer A buyer is identified by a domain in the form "buyerexample.com".
+ * @param name The custom audience's name is an arbitrary string provided by the owner and buyer on
+ * creation of the [CustomAudience] object.
+ * @param dailyUpdateUri a URI that points to a buyer-operated server that hosts updated bidding
+ * data and ads metadata to be used in the on-device ad selection process. The URI must use HTTPS.
+ * @param biddingLogicUri the target URI used to fetch bidding logic when a custom audience
+ * participates in the ad selection process. The URI must use HTTPS.
+ * @param ads the list of [AdData] objects is a full and complete list of the ads that will be
+ * served by this [CustomAudience] during the ad selection process.
+ * @param activationTime optional activation time may be set in the future, in order to serve a
+ * delayed activation. If the field is not set, the object will be activated at the time of joining.
+ * @param expirationTime optional expiration time. Once it has passed, a custom audience is no
+ * longer eligible for daily ad/bidding data updates or participation in the ad selection process.
+ * The custom audience will then be deleted from memory by the next daily update.
+ * @param userBiddingSignals optional User bidding signals, provided by buyers to be consumed by
+ * buyer-provided JavaScript during ad selection in an isolated execution environment.
+ * @param trustedBiddingSignals optional trusted bidding data, consists of a URI pointing to a
+ * trusted server for buyers' bidding data and a list of keys to query the server with.
+ */
+class CustomAudience public constructor(
+    val buyer: AdTechIdentifier,
+    val name: String,
+    val dailyUpdateUri: Uri,
+    val biddingLogicUri: Uri,
+    val ads: List<AdData>,
+    val activationTime: Instant? = null,
+    val expirationTime: Instant? = null,
+    val userBiddingSignals: AdSelectionSignals? = null,
+    val trustedBiddingSignals: TrustedBiddingData? = null
+) {
+
+    /**
+     * Checks whether two [CustomAudience] objects contain the same information.
+     */
+    override fun equals(other: Any?): Boolean {
+        if (this === other) return true
+        if (other !is CustomAudience) return false
+        return this.buyer == other.buyer &&
+            this.name == other.name &&
+            this.activationTime == other.activationTime &&
+            this.expirationTime == other.expirationTime &&
+            this.dailyUpdateUri == other.dailyUpdateUri &&
+            this.userBiddingSignals == other.userBiddingSignals &&
+            this.trustedBiddingSignals == other.trustedBiddingSignals &&
+            this.ads == other.ads
+    }
+
+    /**
+     * Returns the hash of the [CustomAudience] object's data.
+     */
+    override fun hashCode(): Int {
+        var hash = buyer.hashCode()
+        hash = 31 * hash + name.hashCode()
+        hash = 31 * hash + activationTime.hashCode()
+        hash = 31 * hash + expirationTime.hashCode()
+        hash = 31 * hash + dailyUpdateUri.hashCode()
+        hash = 31 * hash + userBiddingSignals.hashCode()
+        hash = 31 * hash + trustedBiddingSignals.hashCode()
+        hash = 31 * hash + biddingLogicUri.hashCode()
+        hash = 31 * hash + ads.hashCode()
+        return hash
+    }
+
+    override fun toString(): String {
+        return "CustomAudience: " +
+            "buyer=$biddingLogicUri, activationTime=$activationTime, " +
+            "expirationTime=$expirationTime, dailyUpdateUri=$dailyUpdateUri, " +
+            "userBiddingSignals=$userBiddingSignals, " +
+            "trustedBiddingSignals=$trustedBiddingSignals, " +
+            "biddingLogicUri=$biddingLogicUri, ads=$ads"
+    }
+
+    /** Builder for [CustomAudience] objects. */
+    @SuppressWarnings("OptionalBuilderConstructorArgument")
+    public class Builder(
+        private var buyer: AdTechIdentifier,
+        private var name: String,
+        private var dailyUpdateUri: Uri,
+        private var biddingLogicUri: Uri,
+        private var ads: List<AdData>
+    ) {
+        private var activationTime: Instant? = null
+        private var expirationTime: Instant? = null
+        private var userBiddingSignals: AdSelectionSignals? = null
+        private var trustedBiddingData: TrustedBiddingData? = null
+
+        /**
+         * Sets the buyer [AdTechIdentifier].
+         *
+         * @param buyer A buyer is identified by a domain in the form "buyerexample.com".
+         */
+        fun setBuyer(buyer: AdTechIdentifier): Builder = apply {
+            this.buyer = buyer
+        }
+
+        /**
+         * Sets the [CustomAudience] object's name.
+         *
+         * @param name  The custom audience's name is an arbitrary string provided by the owner and
+         * buyer on creation of the [CustomAudience] object.
+         */
+        fun setName(name: String): Builder = apply {
+            this.name = name
+        }
+
+        /**
+         * On creation of the [CustomAudience] object, an optional activation time may be set
+         * in the future, in order to serve a delayed activation. If the field is not set, the
+         * [CustomAudience] will be activated at the time of joining.
+         *
+         * For example, a custom audience for lapsed users may not activate until a threshold of
+         * inactivity is reached, at which point the custom audience's ads will participate in the
+         * ad selection process, potentially redirecting lapsed users to the original owner
+         * application.
+         *
+         * The maximum delay in activation is 60 days from initial creation.
+         *
+         * If specified, the activation time must be an earlier instant than the expiration time.
+         *
+         * @param activationTime activation time, truncated to milliseconds, after which the
+         * [CustomAudience] will serve ads.
+         */
+        fun setActivationTime(activationTime: Instant): Builder = apply {
+            this.activationTime = activationTime
+        }
+
+        /**
+         * Once the expiration time has passed, a custom audience is no longer eligible for daily
+         * ad/bidding data updates or participation in the ad selection process. The custom audience
+         * will then be deleted from memory by the next daily update.
+         *
+         * If no expiration time is provided on creation of the [CustomAudience], expiry will
+         * default to 60 days from activation.
+         *
+         * The maximum expiry is 60 days from initial activation.
+         *
+         * @param expirationTime the timestamp [Instant], truncated to milliseconds, after
+         * which the custom audience should be removed.
+         */
+        fun setExpirationTime(expirationTime: Instant): Builder = apply {
+            this.expirationTime = expirationTime
+        }
+
+        /**
+         * This URI points to a buyer-operated server that hosts updated bidding data and ads
+         * metadata to be used in the on-device ad selection process. The URI must use HTTPS.
+         *
+         * @param dailyUpdateUri the custom audience's daily update URI
+         */
+        fun setDailyUpdateUri(dailyUpdateUri: Uri): Builder = apply {
+            this.dailyUpdateUri = dailyUpdateUri
+        }
+
+        /**
+         * User bidding signals are optionally provided by buyers to be consumed by buyer-provided
+         * JavaScript during ad selection in an isolated execution environment.
+         *
+         * If the user bidding signals are not a valid JSON object that can be consumed by the
+         * buyer's JS, the custom audience will not be eligible for ad selection.
+         *
+         * If not specified, the [CustomAudience] will not participate in ad selection
+         * until user bidding signals are provided via the daily update for the custom audience.
+         *
+         * @param userBiddingSignals an [AdSelectionSignals] object representing the user
+         * bidding signals for the custom audience
+         */
+        fun setUserBiddingSignals(userBiddingSignals: AdSelectionSignals): Builder = apply {
+            this.userBiddingSignals = userBiddingSignals
+        }
+
+        /**
+         * Trusted bidding data consists of a URI pointing to a trusted server for buyers' bidding data
+         * and a list of keys to query the server with. Note that the keys are arbitrary identifiers
+         * that will only be used to query the trusted server for a buyer's bidding logic during ad
+         * selection.
+         *
+         * If not specified, the [CustomAudience] will not participate in ad selection
+         * until trusted bidding data are provided via the daily update for the custom audience.
+         *
+         * @param trustedBiddingSignals a [TrustedBiddingData] object containing the custom
+         * audience's trusted bidding data.
+         */
+        @SuppressWarnings("MissingGetterMatchingBuilder")
+        fun setTrustedBiddingData(trustedBiddingSignals: TrustedBiddingData): Builder = apply {
+            this.trustedBiddingData = trustedBiddingSignals
+        }
+
+        /**
+         * Returns the target URI used to fetch bidding logic when a custom audience participates in the
+         * ad selection process. The URI must use HTTPS.
+         *
+         * @param biddingLogicUri the URI for fetching buyer bidding logic
+         */
+        fun setBiddingLogicUri(biddingLogicUri: Uri): Builder = apply {
+            this.biddingLogicUri = biddingLogicUri
+        }
+
+        /**
+         * This list of [AdData] objects is a full and complete list of the ads that will be
+         * served by this [CustomAudience] during the ad selection process.
+         *
+         * If not specified, or if an empty list is provided, the [CustomAudience] will not
+         * participate in ad selection until a valid list of ads are provided via the daily update
+         * for the custom audience.
+         *
+         * @param ads a [List] of [AdData] objects representing ads currently served by
+         * the custom audience.
+         */
+        fun setAds(ads: List<AdData>): Builder = apply {
+            this.ads = ads
+        }
+
+        /**
+         * Builds an instance of a [CustomAudience].
+         */
+        fun build(): CustomAudience {
+            return CustomAudience(
+                buyer,
+                name,
+                dailyUpdateUri,
+                biddingLogicUri,
+                ads,
+                activationTime,
+                expirationTime,
+                userBiddingSignals,
+                trustedBiddingData
+            )
+        }
+    }
+}
diff --git a/privacysandbox/ads/ads-adservices/src/main/java/androidx/privacysandbox/ads/adservices/customaudience/CustomAudienceManager.kt b/privacysandbox/ads/ads-adservices/src/main/java/androidx/privacysandbox/ads/adservices/customaudience/CustomAudienceManager.kt
new file mode 100644
index 0000000..d78c779
--- /dev/null
+++ b/privacysandbox/ads/ads-adservices/src/main/java/androidx/privacysandbox/ads/adservices/customaudience/CustomAudienceManager.kt
@@ -0,0 +1,218 @@
+/*
+ * 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.privacysandbox.ads.adservices.customaudience
+
+import android.adservices.common.AdServicesPermissions
+import android.annotation.SuppressLint
+import android.content.Context
+import android.os.LimitExceededException
+import android.os.ext.SdkExtensions
+import androidx.annotation.DoNotInline
+import androidx.annotation.RequiresExtension
+import androidx.annotation.RequiresPermission
+import androidx.core.os.asOutcomeReceiver
+import androidx.privacysandbox.ads.adservices.common.AdData
+import androidx.privacysandbox.ads.adservices.common.AdSelectionSignals
+import androidx.privacysandbox.ads.adservices.internal.AdServicesInfo
+import androidx.privacysandbox.ads.adservices.common.AdTechIdentifier
+import kotlinx.coroutines.suspendCancellableCoroutine
+
+/**
+ * This class provides APIs for app and ad-SDKs to join / leave custom audiences.
+ */
+abstract class CustomAudienceManager internal constructor() {
+    /**
+     * Adds the user to the given [CustomAudience].
+     *
+     * An attempt to register the user for a custom audience with the same combination of {@code
+     * ownerPackageName}, {@code buyer}, and {@code name} will cause the existing custom audience's
+     * information to be overwritten, including the list of ads data.
+     *
+     * Note that the ads list can be completely overwritten by the daily background fetch job.
+     *
+     * This call fails with an [SecurityException] if
+     *
+     * <ol>
+     *   <li>the {@code ownerPackageName} is not calling app's package name and/or
+     *   <li>the buyer is not authorized to use the API.
+     * </ol>
+     *
+     * This call fails with an [IllegalArgumentException] if
+     *
+     * <ol>
+     *   <li>the storage limit has been exceeded by the calling application and/or
+     *   <li>any URI parameters in the [CustomAudience] given are not authenticated with the
+     *       [CustomAudience] buyer.
+     * </ol>
+     *
+     * This call fails with [LimitExceededException] if the calling package exceeds the
+     * allowed rate limits and is throttled.
+     *
+     * This call fails with an [IllegalStateException] if an internal service error is
+     * encountered.
+     *
+     * @param request The request to join custom audience.
+     */
+    @RequiresPermission(AdServicesPermissions.ACCESS_ADSERVICES_CUSTOM_AUDIENCE)
+    abstract suspend fun joinCustomAudience(request: JoinCustomAudienceRequest)
+
+    /**
+     * Attempts to remove a user from a custom audience by deleting any existing [CustomAudience]
+     * data, identified by {@code ownerPackageName}, {@code buyer}, and {@code
+     * name}.
+     *
+     * This call fails with an [SecurityException] if
+     *
+     * <ol>
+     *   <li>the {@code ownerPackageName} is not calling app's package name; and/or
+     *   <li>the buyer is not authorized to use the API.
+     * </ol>
+     *
+     * This call fails with [LimitExceededException] if the calling package exceeds the
+     * allowed rate limits and is throttled.
+     *
+     * This call does not inform the caller whether the custom audience specified existed in
+     * on-device storage. In other words, it will fail silently when a buyer attempts to leave a
+     * custom audience that was not joined.
+     *
+     * @param request The request to leave custom audience.
+     */
+    @RequiresPermission(AdServicesPermissions.ACCESS_ADSERVICES_CUSTOM_AUDIENCE)
+    abstract suspend fun leaveCustomAudience(request: LeaveCustomAudienceRequest)
+
+    @SuppressLint("ClassVerificationFailure", "NewApi")
+    @RequiresExtension(extension = SdkExtensions.AD_SERVICES, version = 4)
+    private class Api33Ext4Impl(
+        private val customAudienceManager: android.adservices.customaudience.CustomAudienceManager
+        ) : CustomAudienceManager() {
+        constructor(context: Context) : this(
+            context.getSystemService<android.adservices.customaudience.CustomAudienceManager>(
+                android.adservices.customaudience.CustomAudienceManager::class.java
+            )
+        )
+
+        @DoNotInline
+        @RequiresPermission(AdServicesPermissions.ACCESS_ADSERVICES_CUSTOM_AUDIENCE)
+        override suspend fun joinCustomAudience(request: JoinCustomAudienceRequest) {
+            suspendCancellableCoroutine { continuation ->
+                customAudienceManager.joinCustomAudience(
+                    convertJoinRequest(request),
+                    Runnable::run,
+                    continuation.asOutcomeReceiver()
+                )
+            }
+        }
+
+        @DoNotInline
+        @RequiresPermission(AdServicesPermissions.ACCESS_ADSERVICES_CUSTOM_AUDIENCE)
+        override suspend fun leaveCustomAudience(request: LeaveCustomAudienceRequest) {
+            suspendCancellableCoroutine { continuation ->
+                customAudienceManager.leaveCustomAudience(
+                    convertLeaveRequest(request),
+                    Runnable::run,
+                    continuation.asOutcomeReceiver()
+                )
+            }
+        }
+
+        private fun convertJoinRequest(
+            request: JoinCustomAudienceRequest
+        ): android.adservices.customaudience.JoinCustomAudienceRequest {
+            return android.adservices.customaudience.JoinCustomAudienceRequest.Builder()
+                .setCustomAudience(convertCustomAudience(request.customAudience))
+                .build()
+        }
+
+        private fun convertLeaveRequest(
+            request: LeaveCustomAudienceRequest
+        ): android.adservices.customaudience.LeaveCustomAudienceRequest {
+            return android.adservices.customaudience.LeaveCustomAudienceRequest.Builder()
+                .setBuyer(convertAdTechIdentifier(request.buyer))
+                .setName(request.name)
+                .build()
+        }
+
+        private fun convertCustomAudience(
+            request: CustomAudience
+        ): android.adservices.customaudience.CustomAudience {
+            return android.adservices.customaudience.CustomAudience.Builder()
+                .setActivationTime(request.activationTime)
+                .setAds(convertAdData(request.ads))
+                .setBiddingLogicUri(request.biddingLogicUri)
+                .setBuyer(convertAdTechIdentifier(request.buyer))
+                .setDailyUpdateUri(request.dailyUpdateUri)
+                .setExpirationTime(request.expirationTime)
+                .setName(request.name)
+                .setTrustedBiddingData(convertTrustedSignals(request.trustedBiddingSignals))
+                .setUserBiddingSignals(convertBiddingSignals(request.userBiddingSignals))
+                .build()
+        }
+
+        private fun convertAdData(
+            input: List<AdData>
+        ): List<android.adservices.common.AdData> {
+            val result = mutableListOf<android.adservices.common.AdData>()
+            for (ad in input) {
+                result.add(android.adservices.common.AdData.Builder()
+                    .setMetadata(ad.metadata)
+                    .setRenderUri(ad.renderUri)
+                    .build())
+            }
+            return result
+        }
+
+        private fun convertAdTechIdentifier(
+            input: AdTechIdentifier
+        ): android.adservices.common.AdTechIdentifier {
+            return android.adservices.common.AdTechIdentifier.fromString(input.identifier)
+        }
+
+        private fun convertTrustedSignals(
+            input: TrustedBiddingData?
+        ): android.adservices.customaudience.TrustedBiddingData? {
+            if (input == null) return null
+            return android.adservices.customaudience.TrustedBiddingData.Builder()
+                .setTrustedBiddingKeys(input.trustedBiddingKeys)
+                .setTrustedBiddingUri(input.trustedBiddingUri)
+                .build()
+        }
+
+        private fun convertBiddingSignals(
+            input: AdSelectionSignals?
+        ): android.adservices.common.AdSelectionSignals? {
+            if (input == null) return null
+            return android.adservices.common.AdSelectionSignals.fromString(input.signals)
+        }
+    }
+
+    companion object {
+        /**
+         *  Creates [CustomAudienceManager].
+         *
+         *  @return CustomAudienceManager object.
+         */
+        @JvmStatic
+        @SuppressLint("NewApi", "ClassVerificationFailure")
+        fun obtain(context: Context): CustomAudienceManager? {
+            return if (AdServicesInfo.version() >= 4) {
+                Api33Ext4Impl(context)
+            } else {
+                null
+            }
+        }
+    }
+}
diff --git a/privacysandbox/ads/ads-adservices/src/main/java/androidx/privacysandbox/ads/adservices/customaudience/JoinCustomAudienceRequest.kt b/privacysandbox/ads/ads-adservices/src/main/java/androidx/privacysandbox/ads/adservices/customaudience/JoinCustomAudienceRequest.kt
new file mode 100644
index 0000000..11d5703
--- /dev/null
+++ b/privacysandbox/ads/ads-adservices/src/main/java/androidx/privacysandbox/ads/adservices/customaudience/JoinCustomAudienceRequest.kt
@@ -0,0 +1,44 @@
+/*
+ * 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.privacysandbox.ads.adservices.customaudience
+
+/**
+ * The request object to join a custom audience.
+ *
+ * @param customAudience the custom audience to join.
+ */
+class JoinCustomAudienceRequest public constructor(val customAudience: CustomAudience) {
+    /**
+     * Checks whether two [JoinCustomAudienceRequest] objects contain the same information.
+     */
+    override fun equals(other: Any?): Boolean {
+        if (this === other) return true
+        if (other !is JoinCustomAudienceRequest) return false
+        return this.customAudience == other.customAudience
+    }
+
+    /**
+     * Returns the hash of the [JoinCustomAudienceRequest] object's data.
+     */
+    override fun hashCode(): Int {
+        return customAudience.hashCode()
+    }
+
+    override fun toString(): String {
+        return "JoinCustomAudience: customAudience=$customAudience"
+    }
+}
diff --git a/privacysandbox/ads/ads-adservices/src/main/java/androidx/privacysandbox/ads/adservices/customaudience/LeaveCustomAudienceRequest.kt b/privacysandbox/ads/ads-adservices/src/main/java/androidx/privacysandbox/ads/adservices/customaudience/LeaveCustomAudienceRequest.kt
new file mode 100644
index 0000000..ca60ccf
--- /dev/null
+++ b/privacysandbox/ads/ads-adservices/src/main/java/androidx/privacysandbox/ads/adservices/customaudience/LeaveCustomAudienceRequest.kt
@@ -0,0 +1,51 @@
+/*
+ * 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.privacysandbox.ads.adservices.customaudience
+
+import androidx.privacysandbox.ads.adservices.common.AdTechIdentifier
+
+/**
+ * The request object to leave a custom audience.
+ *
+ * @param buyer an [AdTechIdentifier] containing the custom audience's buyer's domain.
+ * @param name the String name of the custom audience.
+ */
+class LeaveCustomAudienceRequest public constructor(
+    val buyer: AdTechIdentifier,
+    val name: String
+    ) {
+
+    /**
+     * Checks whether two [LeaveCustomAudienceRequest] objects contain the same information.
+     */
+    override fun equals(other: Any?): Boolean {
+        if (this === other) return true
+        if (other !is LeaveCustomAudienceRequest) return false
+        return this.buyer == other.buyer && this.name == other.name
+    }
+
+    /**
+     * Returns the hash of the [LeaveCustomAudienceRequest] object's data.
+     */
+    override fun hashCode(): Int {
+        return (31 * buyer.hashCode()) + name.hashCode()
+    }
+
+    override fun toString(): String {
+        return "LeaveCustomAudience: buyer=$buyer, name=$name"
+    }
+}
diff --git a/privacysandbox/ads/ads-adservices/src/main/java/androidx/privacysandbox/ads/adservices/customaudience/TrustedBiddingData.kt b/privacysandbox/ads/ads-adservices/src/main/java/androidx/privacysandbox/ads/adservices/customaudience/TrustedBiddingData.kt
new file mode 100644
index 0000000..fef0a18
--- /dev/null
+++ b/privacysandbox/ads/ads-adservices/src/main/java/androidx/privacysandbox/ads/adservices/customaudience/TrustedBiddingData.kt
@@ -0,0 +1,56 @@
+/*
+ * 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.privacysandbox.ads.adservices.customaudience
+
+import android.net.Uri
+
+/**
+ * Represents data used during the ad selection process to fetch buyer bidding signals from a
+ * trusted key/value server. The fetched data is used during the ad selection process and consumed
+ * by buyer JavaScript logic running in an isolated execution environment.
+ *
+ * @param trustedBiddingUri the URI pointing to the trusted key-value server holding bidding
+ * signals. The URI must use HTTPS.
+ * @param trustedBiddingKeys the list of keys to query from the trusted key-value server holding
+ * bidding signals.
+ */
+class TrustedBiddingData public constructor(
+    val trustedBiddingUri: Uri,
+    val trustedBiddingKeys: List<String>
+    ) {
+    /**
+     * @return `true` if two [TrustedBiddingData] objects contain the same information
+     */
+    override fun equals(other: Any?): Boolean {
+        if (this === other) return true
+        if (other !is TrustedBiddingData) return false
+        return this.trustedBiddingUri == other.trustedBiddingUri &&
+            this.trustedBiddingKeys == other.trustedBiddingKeys
+    }
+
+    /**
+     * @return the hash of the [TrustedBiddingData] object's data
+     */
+    override fun hashCode(): Int {
+        return (31 * trustedBiddingUri.hashCode()) + trustedBiddingKeys.hashCode()
+    }
+
+    override fun toString(): String {
+        return "TrustedBiddingData: trustedBiddingUri=$trustedBiddingUri " +
+            "trustedBiddingKeys=$trustedBiddingKeys"
+    }
+}
\ No newline at end of file
diff --git a/privacysandbox/ads/ads-adservices/src/main/java/androidx/privacysandbox/ads/adservices/internal/AdServicesInfo.kt b/privacysandbox/ads/ads-adservices/src/main/java/androidx/privacysandbox/ads/adservices/internal/AdServicesInfo.kt
new file mode 100644
index 0000000..5f8544d
--- /dev/null
+++ b/privacysandbox/ads/ads-adservices/src/main/java/androidx/privacysandbox/ads/adservices/internal/AdServicesInfo.kt
@@ -0,0 +1,46 @@
+/*
+ * 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.privacysandbox.ads.adservices.internal
+
+import android.os.Build
+import android.os.ext.SdkExtensions
+import androidx.annotation.DoNotInline
+import androidx.annotation.RequiresApi
+
+/**
+ * Temporary replacement for BuildCompat.AD_SERVICES_EXTENSION_INT.
+ * TODO(b/261755947) Replace with AD_SERVICES_EXTENSION_INT after new core library release
+ *
+ * @suppress
+ */
+internal object AdServicesInfo {
+
+    fun version(): Int {
+        return if (Build.VERSION.SDK_INT >= 30) {
+            Extensions30Impl.getAdServicesVersion()
+        } else {
+            0
+        }
+    }
+
+    @RequiresApi(30)
+    private object Extensions30Impl {
+        @DoNotInline
+        fun getAdServicesVersion() =
+            SdkExtensions.getExtensionVersion(SdkExtensions.AD_SERVICES)
+    }
+}
\ No newline at end of file
diff --git a/privacysandbox/ads/ads-adservices/src/main/java/androidx/privacysandbox/ads/adservices/internal/package-info.java b/privacysandbox/ads/ads-adservices/src/main/java/androidx/privacysandbox/ads/adservices/internal/package-info.java
new file mode 100644
index 0000000..5e13308
--- /dev/null
+++ b/privacysandbox/ads/ads-adservices/src/main/java/androidx/privacysandbox/ads/adservices/internal/package-info.java
@@ -0,0 +1,23 @@
+/*
+ * 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.
+ */
+
+/**
+ * @hide
+ */
+@RestrictTo(RestrictTo.Scope.LIBRARY)
+package androidx.privacysandbox.ads.adservices.internal;
+
+import androidx.annotation.RestrictTo;
diff --git a/privacysandbox/ads/ads-adservices/src/main/java/androidx/privacysandbox/ads/adservices/measurement/DeletionRequest.kt b/privacysandbox/ads/ads-adservices/src/main/java/androidx/privacysandbox/ads/adservices/measurement/DeletionRequest.kt
new file mode 100644
index 0000000..581853b
--- /dev/null
+++ b/privacysandbox/ads/ads-adservices/src/main/java/androidx/privacysandbox/ads/adservices/measurement/DeletionRequest.kt
@@ -0,0 +1,212 @@
+/*
+ * 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.privacysandbox.ads.adservices.measurement
+
+import android.net.Uri
+import android.os.Build
+import androidx.annotation.IntDef
+import androidx.annotation.RequiresApi
+import java.time.Instant
+
+/**
+ * Deletion Request.
+ * @param deletionMode Set the deletion mode for the supplied params.
+ *     [DELETION_MODE_ALL]: All data associated with the selected records will be
+ *     deleted.
+ *     [DELETION_MODE_EXCLUDE_INTERNAL_DATA]: All data except the internal system
+ *     data (e.g. rate limits) associated with the selected records will be deleted.
+ *
+ * @param matchBehavior Set the match behavior for the supplied params.
+ *     [MATCH_BEHAVIOR_DELETE]: This option will use the supplied params
+ *     (Origin URIs & Domain URIs) for selecting records for deletion.
+ *     [MATCH_BEHAVIOR_PRESERVE]: This option will preserve the data associated with the
+ *     supplied params (Origin URIs & Domain URIs) and select remaining records for deletion.
+ *
+ * @param start [Instant] Set the start of the deletion range. Not setting this or
+ *     passing in [java.time.Instant#MIN] will cause everything from the oldest record to
+ *     the specified end be deleted.
+ *
+ * @param end [Instant] Set the end of the deletion range. Not setting this or passing in
+ *     [java.time.Instant#MAX] will cause everything from the specified start until the
+ *     newest record to be deleted.
+ *
+ * @param domainUris the list of domain URI which will be used for matching. These will be matched
+ *     with records using the same domain or any subdomains. E.g. If domainUri is {@code
+ *     https://example.com}, then {@code https://a.example.com}, {@code https://example.com} and
+ *     {@code https://b.example.com} will match; {@code https://abcexample.com} will NOT match.
+ *     A null or empty list will match everything.
+ *
+ * @param originUris the list of origin URI which will be used for matching. These will be matched
+ *     with records using the same origin only, i.e. subdomains won't match. E.g. If originUri is
+ *     {@code https://a.example.com}, then {@code https://a.example.com} will match; {@code
+ *     https://example.com}, {@code https://b.example.com} and {@code https://abcexample.com}
+ *     will NOT match. A null or empty list will match everything.
+ */
+@RequiresApi(Build.VERSION_CODES.TIRAMISU)
+class DeletionRequest(
+    @DeletionMode val deletionMode: Int,
+    @MatchBehavior val matchBehavior: Int,
+    val start: Instant = Instant.MIN,
+    val end: Instant = Instant.MAX,
+    val domainUris: List<Uri> = emptyList(),
+    val originUris: List<Uri> = emptyList(),
+) {
+
+    override fun hashCode(): Int {
+        var hash = deletionMode.hashCode()
+        hash = 31 * hash + domainUris.hashCode()
+        hash = 31 * hash + originUris.hashCode()
+        hash = 31 * hash + start.hashCode()
+        hash = 31 * hash + end.hashCode()
+        hash = 31 * hash + matchBehavior.hashCode()
+        return hash
+    }
+
+    override fun equals(other: Any?): Boolean {
+        if (this === other) return true
+        if (other !is DeletionRequest) return false
+        return this.deletionMode == other.deletionMode &&
+            HashSet(this.domainUris) == HashSet(other.domainUris) &&
+            HashSet(this.originUris) == HashSet(other.originUris) &&
+            this.start == other.start &&
+            this.end == other.end &&
+            this.matchBehavior == other.matchBehavior
+    }
+
+    override fun toString(): String {
+        val deletionModeStr = if (deletionMode == DELETION_MODE_ALL) "DELETION_MODE_ALL"
+        else "DELETION_MODE_EXCLUDE_INTERNAL_DATA"
+        val matchBehaviorStr = if (matchBehavior == MATCH_BEHAVIOR_DELETE) "MATCH_BEHAVIOR_DELETE"
+        else "MATCH_BEHAVIOR_PRESERVE"
+        return "DeletionRequest { DeletionMode=$deletionModeStr, " +
+            "MatchBehavior=$matchBehaviorStr, " +
+            "Start=$start, End=$end, DomainUris=$domainUris, OriginUris=$originUris }"
+    }
+
+    companion object {
+        /** Deletion mode to delete all data associated with the selected records.  */
+        public const val DELETION_MODE_ALL = 0
+
+        /**
+         * Deletion mode to delete all data except the internal data (e.g. rate limits) for the
+         * selected records.
+         */
+        public const val DELETION_MODE_EXCLUDE_INTERNAL_DATA = 1
+
+        /** @hide */
+        @Retention(AnnotationRetention.SOURCE)
+        @IntDef(
+            DELETION_MODE_ALL,
+            DELETION_MODE_EXCLUDE_INTERNAL_DATA
+        )
+        annotation class DeletionMode
+
+        /** Match behavior option to delete the supplied params (Origin/Domains).  */
+        public const val MATCH_BEHAVIOR_DELETE = 0
+
+        /**
+         * Match behavior option to preserve the supplied params (Origin/Domains) and delete
+         * everything else.
+         */
+        public const val MATCH_BEHAVIOR_PRESERVE = 1
+
+        /** @hide */
+        @Retention(AnnotationRetention.SOURCE)
+        @IntDef(
+            MATCH_BEHAVIOR_DELETE,
+            MATCH_BEHAVIOR_PRESERVE
+        )
+        annotation class MatchBehavior
+    }
+
+    /**
+     * Builder for {@link DeletionRequest} objects.
+     *
+     * @param deletionMode {@link DeletionMode} Set the match behavior for the supplied params.
+     *     {@link #DELETION_MODE_ALL}: All data associated with the selected records will be
+     *     deleted.
+     *     {@link #DELETION_MODE_EXCLUDE_INTERNAL_DATA}: All data except the internal system
+     *     data (e.g. rate limits) associated with the selected records will be deleted.
+     *
+     * @param matchBehavior {@link MatchBehavior} Set the match behavior for the supplied params.
+     *     {@link #MATCH_BEHAVIOR_DELETE}: This option will use the supplied params
+     *     (Origin URIs & Domain URIs) for selecting records for deletion.
+     *     {@link #MATCH_BEHAVIOR_PRESERVE}: This option will preserve the data associated with the
+     *     supplied params (Origin URIs & Domain URIs) and select remaining records for deletion.
+     */
+    @RequiresApi(Build.VERSION_CODES.TIRAMISU)
+    public class Builder constructor(
+        @DeletionMode private val deletionMode: Int,
+        @MatchBehavior private val matchBehavior: Int
+    ) {
+        private var start: Instant = Instant.MIN
+        private var end: Instant = Instant.MAX
+        private var domainUris: List<Uri> = emptyList()
+        private var originUris: List<Uri> = emptyList()
+
+        /**
+         * Sets the start of the deletion range. Not setting this or passing in
+         * {@link java.time.Instant#MIN} will cause everything from the oldest record to the
+         * specified end be deleted.
+         */
+        fun setStart(start: Instant): Builder = apply {
+            this.start = start
+        }
+
+        /**
+         * Sets the end of the deletion range. Not setting this or passing in
+         * {@link java.time.Instant#MAX} will cause everything from the specified start until the
+         * newest record to be deleted.
+         */
+        fun setEnd(end: Instant): Builder = apply {
+            this.end = end
+        }
+
+        /**
+         * Set the list of domain URI which will be used for matching. These will be matched with
+         * records using the same domain or any subdomains. E.g. If domainUri is {@code
+         * https://example.com}, then {@code https://a.example.com}, {@code https://example.com} and
+         * {@code https://b.example.com} will match; {@code https://abcexample.com} will NOT match.
+         * A null or empty list will match everything.
+         */
+        fun setDomainUris(domainUris: List<Uri>): Builder = apply {
+            this.domainUris = domainUris
+        }
+
+        /**
+         * Set the list of origin URI which will be used for matching. These will be matched with
+         * records using the same origin only, i.e. subdomains won't match. E.g. If originUri is
+         * {@code https://a.example.com}, then {@code https://a.example.com} will match; {@code
+         * https://example.com}, {@code https://b.example.com} and {@code https://abcexample.com}
+         * will NOT match. A null or empty list will match everything.
+         */
+        fun setOriginUris(originUris: List<Uri>): Builder = apply {
+            this.originUris = originUris
+        }
+
+        /** Builds a {@link DeletionRequest} instance. */
+        fun build(): DeletionRequest {
+            return DeletionRequest(
+                deletionMode,
+                matchBehavior,
+                start,
+                end,
+                domainUris,
+                originUris)
+        }
+    }
+}
\ No newline at end of file
diff --git a/privacysandbox/ads/ads-adservices/src/main/java/androidx/privacysandbox/ads/adservices/measurement/MeasurementManager.kt b/privacysandbox/ads/ads-adservices/src/main/java/androidx/privacysandbox/ads/adservices/measurement/MeasurementManager.kt
new file mode 100644
index 0000000..3309244
--- /dev/null
+++ b/privacysandbox/ads/ads-adservices/src/main/java/androidx/privacysandbox/ads/adservices/measurement/MeasurementManager.kt
@@ -0,0 +1,259 @@
+/*
+ * Copyright 2022 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.privacysandbox.ads.adservices.measurement
+
+import android.adservices.common.AdServicesPermissions.ACCESS_ADSERVICES_ATTRIBUTION
+import android.annotation.SuppressLint
+import android.content.Context
+import android.net.Uri
+import android.os.ext.SdkExtensions
+import android.view.InputEvent
+import androidx.annotation.DoNotInline
+import androidx.annotation.RequiresExtension
+import androidx.annotation.RequiresPermission
+import androidx.core.os.asOutcomeReceiver
+import androidx.privacysandbox.ads.adservices.internal.AdServicesInfo
+import kotlinx.coroutines.suspendCancellableCoroutine
+
+/**
+ * This class provides APIs to manage ads attribution using Privacy Sandbox.
+ */
+abstract class MeasurementManager {
+    /**
+     * Delete previous registrations.
+     *
+     * @param deletionRequest The request for deleting data.
+     */
+    abstract suspend fun deleteRegistrations(deletionRequest: DeletionRequest)
+
+    /**
+     * Register an attribution source (click or view).
+     *
+     * @param attributionSource the platform issues a request to this URI in order to fetch metadata
+     *     associated with the attribution source.
+     * @param inputEvent either an [InputEvent] object (for a click event) or null (for a view
+     *     event).
+     */
+    @RequiresPermission(ACCESS_ADSERVICES_ATTRIBUTION)
+    abstract suspend fun registerSource(attributionSource: Uri, inputEvent: InputEvent?)
+
+    /**
+     * Register a trigger (conversion).
+     *
+     * @param trigger the API issues a request to this URI to fetch metadata associated with the
+     *     trigger.
+     */
+    // TODO(b/258551492): Improve docs.
+    @RequiresPermission(ACCESS_ADSERVICES_ATTRIBUTION)
+    abstract suspend fun registerTrigger(trigger: Uri)
+
+    /**
+     * Register an attribution source(click or view) from web context. This API will not process any
+     * redirects, all registration URLs should be supplied with the request. At least one of
+     * appDestination or webDestination parameters are required to be provided.
+     *
+     * @param request source registration request
+     */
+    @RequiresPermission(ACCESS_ADSERVICES_ATTRIBUTION)
+    abstract suspend fun registerWebSource(request: WebSourceRegistrationRequest)
+
+    /**
+     * Register an attribution trigger(click or view) from web context. This API will not process
+     * any redirects, all registration URLs should be supplied with the request.
+     *
+     * @param request trigger registration request
+     */
+    @RequiresPermission(ACCESS_ADSERVICES_ATTRIBUTION)
+    abstract suspend fun registerWebTrigger(request: WebTriggerRegistrationRequest)
+
+    /**
+     * Get Measurement API status.
+     *
+     * The call returns an integer value (see [MEASUREMENT_API_STATE_DISABLED] and
+     * [MEASUREMENT_API_STATE_ENABLED] for possible values).
+     */
+    @RequiresPermission(ACCESS_ADSERVICES_ATTRIBUTION)
+    abstract suspend fun getMeasurementApiStatus(): Int
+
+    @SuppressLint("NewApi", "ClassVerificationFailure")
+    @RequiresExtension(extension = SdkExtensions.AD_SERVICES, version = 4)
+    private class Api33Ext4Impl(
+        private val mMeasurementManager: android.adservices.measurement.MeasurementManager
+    ) : MeasurementManager() {
+        constructor(context: Context) : this(
+            context.getSystemService<android.adservices.measurement.MeasurementManager>(
+                android.adservices.measurement.MeasurementManager::class.java
+            )
+        )
+
+        @DoNotInline
+        override suspend fun deleteRegistrations(deletionRequest: DeletionRequest) {
+            suspendCancellableCoroutine<Any> { continuation ->
+                mMeasurementManager.deleteRegistrations(
+                    convertDeletionRequest(deletionRequest),
+                    Runnable::run,
+                    continuation.asOutcomeReceiver()
+                )
+            }
+        }
+
+        private fun convertDeletionRequest(
+            request: DeletionRequest
+        ): android.adservices.measurement.DeletionRequest {
+            return android.adservices.measurement.DeletionRequest.Builder()
+                .setDeletionMode(request.deletionMode)
+                .setMatchBehavior(request.matchBehavior)
+                .setStart(request.start)
+                .setEnd(request.end)
+                .setDomainUris(request.domainUris)
+                .setOriginUris(request.originUris)
+                .build()
+        }
+
+        @DoNotInline
+        @RequiresPermission(ACCESS_ADSERVICES_ATTRIBUTION)
+        override suspend fun registerSource(attributionSource: Uri, inputEvent: InputEvent?) {
+            suspendCancellableCoroutine<Any> { continuation ->
+                mMeasurementManager.registerSource(
+                    attributionSource,
+                    inputEvent,
+                    Runnable::run,
+                    continuation.asOutcomeReceiver()
+                )
+            }
+        }
+
+        @DoNotInline
+        @RequiresPermission(ACCESS_ADSERVICES_ATTRIBUTION)
+        override suspend fun registerTrigger(trigger: Uri) {
+            suspendCancellableCoroutine<Any> { continuation ->
+                mMeasurementManager.registerTrigger(
+                    trigger,
+                    Runnable::run,
+                    continuation.asOutcomeReceiver())
+            }
+        }
+
+        @DoNotInline
+        @RequiresPermission(ACCESS_ADSERVICES_ATTRIBUTION)
+        override suspend fun registerWebSource(request: WebSourceRegistrationRequest) {
+            suspendCancellableCoroutine<Any> { continuation ->
+                mMeasurementManager.registerWebSource(
+                    convertWebSourceRequest(request),
+                    Runnable::run,
+                    continuation.asOutcomeReceiver())
+            }
+        }
+
+        private fun convertWebSourceRequest(
+            request: WebSourceRegistrationRequest
+        ): android.adservices.measurement.WebSourceRegistrationRequest {
+            return android.adservices.measurement.WebSourceRegistrationRequest
+                .Builder(
+                    convertWebSourceParams(request.webSourceParams),
+                    request.topOriginUri)
+                .setWebDestination(request.webDestination)
+                .setAppDestination(request.appDestination)
+                .setInputEvent(request.inputEvent)
+                .setVerifiedDestination(request.verifiedDestination)
+                .build()
+        }
+
+        private fun convertWebSourceParams(
+            request: List<WebSourceParams>
+        ): List<android.adservices.measurement.WebSourceParams> {
+            var result = mutableListOf<android.adservices.measurement.WebSourceParams>()
+            for (param in request) {
+                result.add(android.adservices.measurement.WebSourceParams
+                    .Builder(param.registrationUri)
+                    .setDebugKeyAllowed(param.debugKeyAllowed)
+                    .build())
+            }
+            return result
+        }
+
+        @DoNotInline
+        @RequiresPermission(ACCESS_ADSERVICES_ATTRIBUTION)
+        override suspend fun registerWebTrigger(request: WebTriggerRegistrationRequest) {
+            suspendCancellableCoroutine<Any> { continuation ->
+                mMeasurementManager.registerWebTrigger(
+                    convertWebTriggerRequest(request),
+                    Runnable::run,
+                    continuation.asOutcomeReceiver())
+            }
+        }
+
+        private fun convertWebTriggerRequest(
+            request: WebTriggerRegistrationRequest
+        ): android.adservices.measurement.WebTriggerRegistrationRequest {
+            return android.adservices.measurement.WebTriggerRegistrationRequest
+                .Builder(
+                    convertWebTriggerParams(request.webTriggerParams),
+                    request.destination)
+                .build()
+        }
+
+        private fun convertWebTriggerParams(
+            request: List<WebTriggerParams>
+        ): List<android.adservices.measurement.WebTriggerParams> {
+            var result = mutableListOf<android.adservices.measurement.WebTriggerParams>()
+            for (param in request) {
+                result.add(android.adservices.measurement.WebTriggerParams
+                    .Builder(param.registrationUri)
+                    .setDebugKeyAllowed(param.debugKeyAllowed)
+                    .build())
+            }
+            return result
+        }
+
+        @DoNotInline
+        @RequiresPermission(ACCESS_ADSERVICES_ATTRIBUTION)
+        override suspend fun getMeasurementApiStatus(): Int = suspendCancellableCoroutine {
+                continuation ->
+            mMeasurementManager.getMeasurementApiStatus(
+                Runnable::run,
+                continuation.asOutcomeReceiver())
+        }
+    }
+
+    companion object {
+        /**
+         * This state indicates that Measurement APIs are unavailable. Invoking them will result
+         * in an [UnsupportedOperationException].
+         */
+        public const val MEASUREMENT_API_STATE_DISABLED = 0
+        /**
+         * This state indicates that Measurement APIs are enabled.
+         */
+        public const val MEASUREMENT_API_STATE_ENABLED = 1
+
+        /**
+         *  Creates [MeasurementManager].
+         *
+         *  @return MeasurementManager object.
+         */
+        @JvmStatic
+        @SuppressLint("NewApi", "ClassVerificationFailure")
+        fun obtain(context: Context): MeasurementManager? {
+            return if (AdServicesInfo.version() >= 4) {
+                Api33Ext4Impl(context)
+            } else {
+                null
+            }
+        }
+    }
+}
diff --git a/privacysandbox/ads/ads-adservices/src/main/java/androidx/privacysandbox/ads/adservices/measurement/WebSourceParams.kt b/privacysandbox/ads/ads-adservices/src/main/java/androidx/privacysandbox/ads/adservices/measurement/WebSourceParams.kt
new file mode 100644
index 0000000..b37465a
--- /dev/null
+++ b/privacysandbox/ads/ads-adservices/src/main/java/androidx/privacysandbox/ads/adservices/measurement/WebSourceParams.kt
@@ -0,0 +1,54 @@
+/*
+ * Copyright 2022 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.privacysandbox.ads.adservices.measurement
+
+import android.net.Uri
+import android.os.Build
+import androidx.annotation.RequiresApi
+
+/**
+ * Class holding source registration parameters.
+ *
+ * @param registrationUri URI that the Attribution Reporting API sends a request to in order to
+ * obtain source registration parameters.
+ * @param debugKeyAllowed Used by the browser to indicate whether the debug key obtained from the
+ * registration URI is allowed to be used.
+ */
+@RequiresApi(Build.VERSION_CODES.TIRAMISU)
+class WebSourceParams public constructor(
+    val registrationUri: Uri,
+    val debugKeyAllowed: Boolean
+    ) {
+
+    override fun equals(other: Any?): Boolean {
+        if (this === other) return true
+        if (other !is WebSourceParams) return false
+        return this.registrationUri == other.registrationUri &&
+            this.debugKeyAllowed == other.debugKeyAllowed
+    }
+
+    override fun hashCode(): Int {
+        var hash = registrationUri.hashCode()
+        hash = 31 * hash + debugKeyAllowed.hashCode()
+        return hash
+    }
+
+    override fun toString(): String {
+        return "WebSourceParams { RegistrationUri=$registrationUri, " +
+            "DebugKeyAllowed=$debugKeyAllowed }"
+    }
+}
\ No newline at end of file
diff --git a/privacysandbox/ads/ads-adservices/src/main/java/androidx/privacysandbox/ads/adservices/measurement/WebSourceRegistrationRequest.kt b/privacysandbox/ads/ads-adservices/src/main/java/androidx/privacysandbox/ads/adservices/measurement/WebSourceRegistrationRequest.kt
new file mode 100644
index 0000000..c85be19
--- /dev/null
+++ b/privacysandbox/ads/ads-adservices/src/main/java/androidx/privacysandbox/ads/adservices/measurement/WebSourceRegistrationRequest.kt
@@ -0,0 +1,162 @@
+/*
+ * 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.privacysandbox.ads.adservices.measurement
+
+import android.net.Uri
+import android.os.Build
+import android.view.InputEvent
+import androidx.annotation.RequiresApi
+
+/**
+ * Class to hold input to measurement source registration calls from web context.
+ *
+ * @param webSourceParams Registration info to fetch sources.
+ * @param topOriginUri Top level origin of publisher.
+ * @param inputEvent User Interaction {@link InputEvent} used by the AttributionReporting API to
+ * distinguish clicks from views.
+ * @param appDestination App destination of the source. It is the android app {@link Uri} where
+ * corresponding conversion is expected. At least one of app destination or web destination is
+ * required.
+ * @param webDestination Web destination of the source. It is the website {@link Uri} where
+ * corresponding conversion is expected. At least one of app destination or web destination is
+ * required.
+ * @param verifiedDestination Verified destination by the caller. This is where the user actually
+ * landed.
+ */
+@RequiresApi(Build.VERSION_CODES.TIRAMISU)
+class WebSourceRegistrationRequest public constructor(
+    val webSourceParams: List<WebSourceParams>,
+    val topOriginUri: Uri,
+    val inputEvent: InputEvent? = null,
+    val appDestination: Uri? = null,
+    val webDestination: Uri? = null,
+    val verifiedDestination: Uri? = null
+    ) {
+
+    override fun equals(other: Any?): Boolean {
+        if (this === other) return true
+        if (other !is WebSourceRegistrationRequest) return false
+        return this.webSourceParams == other.webSourceParams &&
+            this.webDestination == other.webDestination &&
+            this.appDestination == other.appDestination &&
+            this.topOriginUri == other.topOriginUri &&
+            this.inputEvent == other.inputEvent &&
+            this.verifiedDestination == other.verifiedDestination
+    }
+
+    override fun hashCode(): Int {
+        var hash = webSourceParams.hashCode()
+        hash = 31 * hash + topOriginUri.hashCode()
+        if (inputEvent != null) {
+            hash = 31 * hash + inputEvent.hashCode()
+        }
+        if (appDestination != null) {
+            hash = 31 * hash + appDestination.hashCode()
+        }
+        if (webDestination != null) {
+            hash = 31 * hash + webDestination.hashCode()
+        }
+        // Since topOriginUri is non-null.
+        hash = 31 * hash + topOriginUri.hashCode()
+        if (inputEvent != null) {
+            hash = 31 * hash + inputEvent.hashCode()
+        }
+        if (verifiedDestination != null) {
+            hash = 31 * hash + verifiedDestination.hashCode()
+        }
+        return hash
+    }
+
+    override fun toString(): String {
+        val vals = "WebSourceParams=[$webSourceParams], TopOriginUri=$topOriginUri, " +
+            "InputEvent=$inputEvent, AppDestination=$appDestination, " +
+            "WebDestination=$webDestination, VerifiedDestination=$verifiedDestination"
+        return "WebSourceRegistrationRequest { $vals }"
+    }
+
+    /**
+     * Builder for [WebSourceRegistrationRequest].
+     *
+     * @param webSourceParams source parameters containing source registration parameters, the
+     *     list should not be empty
+     * @param topOriginUri source publisher [Uri]
+     */
+    public class Builder(
+        private val webSourceParams: List<WebSourceParams>,
+        private val topOriginUri: Uri
+    ) {
+        private var inputEvent: InputEvent? = null
+        private var appDestination: Uri? = null
+        private var webDestination: Uri? = null
+        private var verifiedDestination: Uri? = null
+
+        /**
+         * Setter for input event.
+         *
+         * @param inputEvent User Interaction InputEvent used by the AttributionReporting API to
+         *     distinguish clicks from views.
+         * @return builder
+         */
+        fun setInputEvent(inputEvent: InputEvent): Builder = apply {
+            this.inputEvent = inputEvent
+        }
+
+        /**
+         * Setter for app destination. It is the android app {@link Uri} where corresponding
+         * conversion is expected. At least one of app destination or web destination is required.
+         *
+         * @param appDestination app destination [Uri]
+         * @return builder
+         */
+        fun setAppDestination(appDestination: Uri?): Builder = apply {
+            this.appDestination = appDestination
+        }
+
+        /**
+         * Setter for web destination. It is the website {@link Uri} where corresponding conversion
+         * is expected. At least one of app destination or web destination is required.
+         *
+         * @param webDestination web destination [Uri]
+         * @return builder
+         */
+        fun setWebDestination(webDestination: Uri?): Builder = apply {
+            this.webDestination = webDestination
+        }
+
+        /**
+         * Setter for verified destination.
+         *
+         * @param verifiedDestination verified destination
+         * @return builder
+         */
+        fun setVerifiedDestination(verifiedDestination: Uri?): Builder = apply {
+            this.verifiedDestination = verifiedDestination
+        }
+
+        /** Pre-validates parameters and builds [WebSourceRegistrationRequest]. */
+        fun build(): WebSourceRegistrationRequest {
+            return WebSourceRegistrationRequest(
+                webSourceParams,
+                topOriginUri,
+                inputEvent,
+                appDestination,
+                webDestination,
+                verifiedDestination
+            )
+        }
+    }
+}
\ No newline at end of file
diff --git a/privacysandbox/ads/ads-adservices/src/main/java/androidx/privacysandbox/ads/adservices/measurement/WebTriggerParams.kt b/privacysandbox/ads/ads-adservices/src/main/java/androidx/privacysandbox/ads/adservices/measurement/WebTriggerParams.kt
new file mode 100644
index 0000000..ec91bda
--- /dev/null
+++ b/privacysandbox/ads/ads-adservices/src/main/java/androidx/privacysandbox/ads/adservices/measurement/WebTriggerParams.kt
@@ -0,0 +1,54 @@
+/*
+ * Copyright 2022 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.privacysandbox.ads.adservices.measurement
+
+import android.net.Uri
+import android.os.Build
+import androidx.annotation.RequiresApi
+
+/**
+ * Class holding trigger registration parameters.
+ *
+ * @param registrationUri URI that the Attribution Reporting API sends a request to in order to
+ * obtain trigger registration parameters.
+ * @param debugKeyAllowed Used by the browser to indicate whether the debug key obtained from the
+ * registration URI is allowed to be used.
+ */
+@RequiresApi(Build.VERSION_CODES.TIRAMISU)
+class WebTriggerParams public constructor(
+    val registrationUri: Uri,
+    val debugKeyAllowed: Boolean
+    ) {
+
+    override fun equals(other: Any?): Boolean {
+        if (this === other) return true
+        if (other !is WebTriggerParams) return false
+        return this.registrationUri == other.registrationUri &&
+            this.debugKeyAllowed == other.debugKeyAllowed
+    }
+
+    override fun hashCode(): Int {
+        var hash = registrationUri.hashCode()
+        hash = 31 * hash + debugKeyAllowed.hashCode()
+        return hash
+    }
+
+    override fun toString(): String {
+        return "WebTriggerParams { RegistrationUri=$registrationUri, " +
+            "DebugKeyAllowed=$debugKeyAllowed }"
+    }
+}
diff --git a/privacysandbox/ads/ads-adservices/src/main/java/androidx/privacysandbox/ads/adservices/measurement/WebTriggerRegistrationRequest.kt b/privacysandbox/ads/ads-adservices/src/main/java/androidx/privacysandbox/ads/adservices/measurement/WebTriggerRegistrationRequest.kt
new file mode 100644
index 0000000..6cbd612
--- /dev/null
+++ b/privacysandbox/ads/ads-adservices/src/main/java/androidx/privacysandbox/ads/adservices/measurement/WebTriggerRegistrationRequest.kt
@@ -0,0 +1,51 @@
+/*
+ * 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.privacysandbox.ads.adservices.measurement
+
+import android.net.Uri
+import android.os.Build
+import androidx.annotation.RequiresApi
+
+/**
+ * Class to hold input to measurement trigger registration calls from web context.
+ *
+ * @param webTriggerParams Registration info to fetch sources.
+ * @param destination Destination [Uri].
+ */
+@RequiresApi(Build.VERSION_CODES.TIRAMISU)
+class WebTriggerRegistrationRequest public constructor(
+    val webTriggerParams: List<WebTriggerParams>,
+    val destination: Uri
+    ) {
+    override fun equals(other: Any?): Boolean {
+        if (this === other) return true
+        if (other !is WebTriggerRegistrationRequest) return false
+        return this.webTriggerParams == other.webTriggerParams &&
+            this.destination == other.destination
+    }
+
+    override fun hashCode(): Int {
+        var hash = webTriggerParams.hashCode()
+        hash = 31 * hash + destination.hashCode()
+        return hash
+    }
+
+    override fun toString(): String {
+        return "WebTriggerRegistrationRequest { WebTriggerParams=$webTriggerParams, " +
+            "Destination=$destination"
+    }
+}
\ No newline at end of file
diff --git a/privacysandbox/ads/ads-adservices/src/main/java/androidx/privacysandbox/ads/adservices/package-info.java b/privacysandbox/ads/ads-adservices/src/main/java/androidx/privacysandbox/ads/adservices/package-info.java
new file mode 100644
index 0000000..2a6c0d8
--- /dev/null
+++ b/privacysandbox/ads/ads-adservices/src/main/java/androidx/privacysandbox/ads/adservices/package-info.java
@@ -0,0 +1,20 @@
+/*
+ * Copyright (C) 2022 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+/**
+ * Privacy Preserving APIs for Privacy Sandbox.
+ */
+package androidx.privacysandbox.ads.adservices;
diff --git a/privacysandbox/ads/ads-adservices/src/main/java/androidx/privacysandbox/ads/adservices/topics/GetTopicsRequest.kt b/privacysandbox/ads/ads-adservices/src/main/java/androidx/privacysandbox/ads/adservices/topics/GetTopicsRequest.kt
new file mode 100644
index 0000000..1bfa0d37
--- /dev/null
+++ b/privacysandbox/ads/ads-adservices/src/main/java/androidx/privacysandbox/ads/adservices/topics/GetTopicsRequest.kt
@@ -0,0 +1,87 @@
+/*
+ * 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.privacysandbox.ads.adservices.topics
+
+/**
+ * Represents the request for the getTopics API (which takes a [GetTopicsRequest] and
+ * returns a [GetTopicsResponse].
+ *
+ * @param sdkName The Ads SDK name. This must be called by SDKs running outside of the Sandbox.
+ * Other clients must not call it.
+ * @param shouldRecordObservation whether to record that the caller has observed the topics of the
+ *     host app or not. This will be used to determine if the caller can receive the topic
+ *     in the next epoch.
+ */
+class GetTopicsRequest public constructor(
+    val sdkName: String = "",
+    @get:JvmName("shouldRecordObservation")
+    val shouldRecordObservation: Boolean = false
+) {
+    override fun toString(): String {
+        return "GetTopicsRequest: " +
+            "sdkName=$sdkName, shouldRecordObservation=$shouldRecordObservation"
+    }
+
+    override fun equals(other: Any?): Boolean {
+        if (this === other) return true
+        if (other !is GetTopicsRequest) return false
+        return this.sdkName == other.sdkName &&
+            this.shouldRecordObservation == other.shouldRecordObservation
+    }
+
+    override fun hashCode(): Int {
+        var hash = sdkName.hashCode()
+        hash = 31 * hash + shouldRecordObservation.hashCode()
+        return hash
+    }
+
+    /**
+     * Builder for [GetTopicsRequest].
+     */
+    public class Builder() {
+        private var sdkName: String = ""
+        private var shouldRecordObservation: Boolean = true
+
+        /**
+         * Set Ads Sdk Name.
+         *
+         * <p>This must be called by SDKs running outside of the Sandbox. Other clients must not
+         * call it.
+         *
+         * @param sdkName the Ads Sdk Name.
+         */
+        fun setSdkName(sdkName: String): Builder = apply { this.sdkName = sdkName }
+
+        /**
+         * Set the Record Observation.
+         *
+         * @param shouldRecordObservation whether to record that the caller has observed the topics of the
+         *     host app or not. This will be used to determine if the caller can receive the topic
+         *     in the next epoch.
+         */
+        @Suppress("MissingGetterMatchingBuilder")
+        fun setShouldRecordObservation(shouldRecordObservation: Boolean): Builder = apply {
+            this.shouldRecordObservation = shouldRecordObservation
+        }
+
+        /** Builds a [GetTopicsRequest] instance. */
+        fun build(): GetTopicsRequest {
+            check(sdkName.isNotEmpty()) { "sdkName must be set" }
+            return GetTopicsRequest(sdkName, shouldRecordObservation)
+        }
+    }
+}
\ No newline at end of file
diff --git a/privacysandbox/ads/ads-adservices/src/main/java/androidx/privacysandbox/ads/adservices/topics/GetTopicsResponse.kt b/privacysandbox/ads/ads-adservices/src/main/java/androidx/privacysandbox/ads/adservices/topics/GetTopicsResponse.kt
new file mode 100644
index 0000000..78e871b
--- /dev/null
+++ b/privacysandbox/ads/ads-adservices/src/main/java/androidx/privacysandbox/ads/adservices/topics/GetTopicsResponse.kt
@@ -0,0 +1,37 @@
+/*
+ * 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.privacysandbox.ads.adservices.topics
+
+import java.util.Objects
+
+/** Represent the result from the getTopics API. */
+class GetTopicsResponse(val topics: List<Topic>) {
+    override fun equals(other: Any?): Boolean {
+        if (this === other) return true
+        if (other !is GetTopicsResponse) return false
+        if (topics.size != other.topics.size) return false
+        return HashSet(this.topics) == HashSet(other.topics)
+    }
+
+    override fun hashCode(): Int {
+        return Objects.hash(topics)
+    }
+
+    override fun toString(): String {
+        return "Topics=$topics"
+    }
+}
\ No newline at end of file
diff --git a/privacysandbox/ads/ads-adservices/src/main/java/androidx/privacysandbox/ads/adservices/topics/Topic.kt b/privacysandbox/ads/ads-adservices/src/main/java/androidx/privacysandbox/ads/adservices/topics/Topic.kt
new file mode 100644
index 0000000..69ab3ec
--- /dev/null
+++ b/privacysandbox/ads/ads-adservices/src/main/java/androidx/privacysandbox/ads/adservices/topics/Topic.kt
@@ -0,0 +1,53 @@
+/*
+ * 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.privacysandbox.ads.adservices.topics
+
+/**
+ * Represent the topic result from the getTopics API.
+ *
+ * @param taxonomyVersion the version of the taxonomy.
+ * @param modelVersion the version of the model.
+ * @param topicId the unique id of a topic.
+ * See https://developer.android.com/design-for-safety/privacy-sandbox/guides/topics for details.
+ */
+class Topic public constructor(
+    val taxonomyVersion: Long,
+    val modelVersion: Long,
+    val topicId: Int
+) {
+    override fun toString(): String {
+        val taxonomyVersionString = "TaxonomyVersion=$taxonomyVersion" +
+            ", ModelVersion=$modelVersion" +
+            ", TopicCode=$topicId }"
+        return "Topic { $taxonomyVersionString"
+    }
+
+    override fun equals(other: Any?): Boolean {
+        if (this === other) return true
+        if (other !is Topic) return false
+        return this.taxonomyVersion == other.taxonomyVersion &&
+            this.modelVersion == other.modelVersion &&
+            this.topicId == other.topicId
+    }
+
+    override fun hashCode(): Int {
+        var hash = taxonomyVersion.hashCode()
+        hash = 31 * hash + modelVersion.hashCode()
+        hash = 31 * hash + topicId.hashCode()
+        return hash
+    }
+}
\ No newline at end of file
diff --git a/privacysandbox/ads/ads-adservices/src/main/java/androidx/privacysandbox/ads/adservices/topics/TopicsManager.kt b/privacysandbox/ads/ads-adservices/src/main/java/androidx/privacysandbox/ads/adservices/topics/TopicsManager.kt
new file mode 100644
index 0000000..67d7764
--- /dev/null
+++ b/privacysandbox/ads/ads-adservices/src/main/java/androidx/privacysandbox/ads/adservices/topics/TopicsManager.kt
@@ -0,0 +1,116 @@
+/*
+ * 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.privacysandbox.ads.adservices.topics
+
+import android.adservices.common.AdServicesPermissions
+import android.annotation.SuppressLint
+import android.content.Context
+import android.os.LimitExceededException
+import android.os.ext.SdkExtensions
+import androidx.annotation.DoNotInline
+import androidx.annotation.RequiresExtension
+import androidx.annotation.RequiresPermission
+import androidx.core.os.asOutcomeReceiver
+import androidx.privacysandbox.ads.adservices.internal.AdServicesInfo
+import kotlinx.coroutines.suspendCancellableCoroutine
+
+/**
+ * TopicsManager provides APIs for App and Ad-Sdks to get the user interest topics in a privacy
+ * preserving way.
+ */
+abstract class TopicsManager internal constructor() {
+    /**
+     * Return the topics.
+     *
+     * @param request The GetTopicsRequest for obtaining Topics.
+     * @throws SecurityException if caller is not authorized to call this API.
+     * @throws IllegalStateException if this API is not available.
+     * @throws LimitExceededException if rate limit was reached.
+     * @return GetTopicsResponse
+     */
+    @RequiresPermission(AdServicesPermissions.ACCESS_ADSERVICES_TOPICS)
+    abstract suspend fun getTopics(request: GetTopicsRequest): GetTopicsResponse
+
+    @SuppressLint("NewApi", "ClassVerificationFailure")
+    @RequiresExtension(extension = SdkExtensions.AD_SERVICES, version = 4)
+    private class Api33Ext4Impl(
+        private val mTopicsManager: android.adservices.topics.TopicsManager
+        ) : TopicsManager() {
+        constructor(context: Context) : this(
+            context.getSystemService<android.adservices.topics.TopicsManager>(
+                android.adservices.topics.TopicsManager::class.java
+            )
+        )
+
+        @DoNotInline
+        @RequiresPermission(AdServicesPermissions.ACCESS_ADSERVICES_TOPICS)
+        override suspend fun getTopics(request: GetTopicsRequest): GetTopicsResponse {
+            return convertResponse(getTopicsAsyncInternal(convertRequest(request)))
+        }
+
+        @RequiresPermission(AdServicesPermissions.ACCESS_ADSERVICES_TOPICS)
+        private suspend fun getTopicsAsyncInternal(
+            getTopicsRequest: android.adservices.topics.GetTopicsRequest
+        ): android.adservices.topics.GetTopicsResponse = suspendCancellableCoroutine { continuation
+            ->
+            mTopicsManager.getTopics(
+                getTopicsRequest,
+                Runnable::run,
+                continuation.asOutcomeReceiver()
+            )
+        }
+
+        private fun convertRequest(
+            request: GetTopicsRequest
+        ): android.adservices.topics.GetTopicsRequest {
+            if (!request.shouldRecordObservation) {
+                throw IllegalArgumentException("shouldRecordObservation not supported yet.")
+            }
+            return android.adservices.topics.GetTopicsRequest.Builder()
+                .setAdsSdkName(request.sdkName)
+                .build()
+        }
+
+        internal fun convertResponse(
+            response: android.adservices.topics.GetTopicsResponse
+        ): GetTopicsResponse {
+            var topics = mutableListOf<Topic>()
+            for (topic in response.topics) {
+                topics.add(Topic(topic.taxonomyVersion, topic.modelVersion, topic.topicId))
+            }
+            return GetTopicsResponse(topics)
+        }
+    }
+
+    companion object {
+        /**
+         *  Creates [TopicsManager].
+         *
+         *  @return TopicsManagerCompat object. If the device is running an incompatible
+         *  build, the value returned is null.
+         */
+        @JvmStatic
+        @SuppressLint("NewApi", "ClassVerificationFailure")
+        fun obtain(context: Context): TopicsManager? {
+            return if (AdServicesInfo.version() >= 4) {
+                Api33Ext4Impl(context)
+            } else {
+                null
+            }
+        }
+    }
+}
\ No newline at end of file
diff --git a/privacysandbox/tools/tools-apicompiler/src/main/java/androidx/privacysandbox/tools/apicompiler/parser/ApiParser.kt b/privacysandbox/tools/tools-apicompiler/src/main/java/androidx/privacysandbox/tools/apicompiler/parser/ApiParser.kt
index 5782e08..19c669b 100644
--- a/privacysandbox/tools/tools-apicompiler/src/main/java/androidx/privacysandbox/tools/apicompiler/parser/ApiParser.kt
+++ b/privacysandbox/tools/tools-apicompiler/src/main/java/androidx/privacysandbox/tools/apicompiler/parser/ApiParser.kt
@@ -40,7 +40,7 @@
 /** Top-level entry point to parse a complete user-defined sandbox SDK API into a [ParsedApi]. */
 class ApiParser(private val resolver: Resolver, private val logger: KSPLogger) {
     private val typeParser = TypeParser(logger)
-    private val interfaceParser = InterfaceParser(logger, typeParser)
+    private val interfaceParser = InterfaceParser(logger, typeParser, resolver)
     private val valueParser = ValueParser(logger, typeParser)
 
     fun parseApi(): ParsedApi {
diff --git a/privacysandbox/tools/tools-apicompiler/src/main/java/androidx/privacysandbox/tools/apicompiler/parser/InterfaceParser.kt b/privacysandbox/tools/tools-apicompiler/src/main/java/androidx/privacysandbox/tools/apicompiler/parser/InterfaceParser.kt
index cb2b9d0..7455049 100644
--- a/privacysandbox/tools/tools-apicompiler/src/main/java/androidx/privacysandbox/tools/apicompiler/parser/InterfaceParser.kt
+++ b/privacysandbox/tools/tools-apicompiler/src/main/java/androidx/privacysandbox/tools/apicompiler/parser/InterfaceParser.kt
@@ -23,13 +23,18 @@
 import com.google.devtools.ksp.getDeclaredProperties
 import com.google.devtools.ksp.isPublic
 import com.google.devtools.ksp.processing.KSPLogger
+import com.google.devtools.ksp.processing.Resolver
 import com.google.devtools.ksp.symbol.ClassKind
 import com.google.devtools.ksp.symbol.KSClassDeclaration
 import com.google.devtools.ksp.symbol.KSFunctionDeclaration
 import com.google.devtools.ksp.symbol.KSValueParameter
 import com.google.devtools.ksp.symbol.Modifier
 
-internal class InterfaceParser(private val logger: KSPLogger, private val typeParser: TypeParser) {
+internal class InterfaceParser(
+    private val logger: KSPLogger,
+    private val typeParser: TypeParser,
+    private val resolver: Resolver,
+) {
     private val validInterfaceModifiers = setOf(Modifier.PUBLIC)
     private val validMethodModifiers = setOf(Modifier.PUBLIC, Modifier.SUSPEND)
 
@@ -71,6 +76,17 @@
                 })."
             )
         }
+        if (interfaceDeclaration.superTypes.singleOrNull()?.resolve()
+            != resolver.builtIns.anyType
+        ) {
+            logger.error(
+                "Error in $name: annotated interface inherits prohibited types (${
+                    interfaceDeclaration.superTypes.map {
+                        it.resolve().declaration.simpleName.getShortName()
+                    }.sorted().joinToString(limit = 3)
+                })."
+            )
+        }
 
         val methods = interfaceDeclaration.getDeclaredFunctions().map(::parseMethod).toList()
         return AnnotatedInterface(
diff --git a/privacysandbox/tools/tools-apicompiler/src/test/java/androidx/privacysandbox/tools/apicompiler/parser/InterfaceParserTest.kt b/privacysandbox/tools/tools-apicompiler/src/test/java/androidx/privacysandbox/tools/apicompiler/parser/InterfaceParserTest.kt
index 93b2d53..776d7b3 100644
--- a/privacysandbox/tools/tools-apicompiler/src/test/java/androidx/privacysandbox/tools/apicompiler/parser/InterfaceParserTest.kt
+++ b/privacysandbox/tools/tools-apicompiler/src/test/java/androidx/privacysandbox/tools/apicompiler/parser/InterfaceParserTest.kt
@@ -202,6 +202,49 @@
     }
 
     @Test
+    fun interfaceInheritance_fails() {
+        val source = Source.kotlin(
+            "com/mysdk/MySdk.kt", """
+                    package com.mysdk
+                    import androidx.privacysandbox.tools.PrivacySandboxService
+
+                    interface FooInterface {}
+
+                    @PrivacySandboxService
+                    interface MySdk : FooInterface {
+                        suspend fun foo(): Int
+                    }"""
+        )
+        checkSourceFails(source).containsExactlyErrors(
+            "Error in com.mysdk.MySdk: annotated interface inherits prohibited types (" +
+                "FooInterface)."
+        )
+    }
+
+    @Test
+    fun interfaceInheritsManyInterfaces_fails() {
+        val source = Source.kotlin(
+            "com/mysdk/MySdk.kt", """
+                    package com.mysdk
+                    import androidx.privacysandbox.tools.PrivacySandboxService
+
+                    interface A {}
+                    interface B {}
+                    interface C {}
+                    interface D {}
+
+                    @PrivacySandboxService
+                    interface MySdk : B, C, D, A {
+                        suspend fun foo(): Int
+                    }"""
+        )
+        checkSourceFails(source).containsExactlyErrors(
+            "Error in com.mysdk.MySdk: annotated interface inherits prohibited types (A, B, C, " +
+                "...)."
+        )
+    }
+
+    @Test
     fun methodWithImplementation_fails() {
         checkSourceFails(serviceMethod("suspend fun foo(): Int = 1")).containsExactlyErrors(
             "Error in com.mysdk.MySdk.foo: method cannot have default implementation."
@@ -223,7 +266,7 @@
     }
 
     @Test
-    fun parameterWitDefaultValue_fails() {
+    fun parameterWithDefaultValue_fails() {
         checkSourceFails(serviceMethod("suspend fun foo(x: Int = 5)")).containsExactlyErrors(
             "Error in com.mysdk.MySdk.foo: parameters cannot have default values."
         )
diff --git a/room/integration-tests/kotlintestapp/src/androidTest/java/androidx/room/integration/kotlintestapp/test/LiveDataTestUtil.kt b/room/integration-tests/kotlintestapp/src/androidTest/java/androidx/room/integration/kotlintestapp/test/LiveDataTestUtil.kt
index aabc29a..98b9f79 100644
--- a/room/integration-tests/kotlintestapp/src/androidTest/java/androidx/room/integration/kotlintestapp/test/LiveDataTestUtil.kt
+++ b/room/integration-tests/kotlintestapp/src/androidTest/java/androidx/room/integration/kotlintestapp/test/LiveDataTestUtil.kt
@@ -29,8 +29,8 @@
         val latch = CountDownLatch(1)
         var data: T? = null
         val observer = object : Observer<T> {
-            override fun onChanged(o: T?) {
-                data = o
+            override fun onChanged(value: T) {
+                data = value
                 liveData.removeObserver(this)
                 latch.countDown()
             }
diff --git a/room/integration-tests/kotlintestapp/src/androidTest/java/androidx/room/integration/kotlintestapp/testutil/TestObserver.kt b/room/integration-tests/kotlintestapp/src/androidTest/java/androidx/room/integration/kotlintestapp/testutil/TestObserver.kt
index c3005bf..2b89222 100644
--- a/room/integration-tests/kotlintestapp/src/androidTest/java/androidx/room/integration/kotlintestapp/testutil/TestObserver.kt
+++ b/room/integration-tests/kotlintestapp/src/androidTest/java/androidx/room/integration/kotlintestapp/testutil/TestObserver.kt
@@ -27,8 +27,8 @@
         mLastData = null
     }
 
-    override fun onChanged(o: T?) {
-        mLastData = o
+    override fun onChanged(value: T) {
+        mLastData = value
         mHasValue = true
     }
 
diff --git a/room/room-common/api/current.txt b/room/room-common/api/current.txt
index ad75209..6072678 100644
--- a/room/room-common/api/current.txt
+++ b/room/room-common/api/current.txt
@@ -377,6 +377,7 @@
     field public static final String PRIMARY_KEY_FROM_EMBEDDED_IS_DROPPED = "ROOM_EMBEDDED_PRIMARY_KEY_IS_DROPPED";
     field public static final String RELATION_QUERY_WITHOUT_TRANSACTION = "ROOM_RELATION_QUERY_WITHOUT_TRANSACTION";
     field public static final String RELATION_TYPE_MISMATCH = "ROOM_RELATION_TYPE_MISMATCH";
+    field public static final String ROOM_UNNECESSARY_NULLABILITY_IN_DAO_RETURN_TYPE = "UNNECESSARY_NULLABILITY_IN_DAO_RETURN_TYPE";
   }
 
   public static final class RoomWarnings.Companion {
diff --git a/room/room-common/api/public_plus_experimental_current.txt b/room/room-common/api/public_plus_experimental_current.txt
index ad75209..6072678 100644
--- a/room/room-common/api/public_plus_experimental_current.txt
+++ b/room/room-common/api/public_plus_experimental_current.txt
@@ -377,6 +377,7 @@
     field public static final String PRIMARY_KEY_FROM_EMBEDDED_IS_DROPPED = "ROOM_EMBEDDED_PRIMARY_KEY_IS_DROPPED";
     field public static final String RELATION_QUERY_WITHOUT_TRANSACTION = "ROOM_RELATION_QUERY_WITHOUT_TRANSACTION";
     field public static final String RELATION_TYPE_MISMATCH = "ROOM_RELATION_TYPE_MISMATCH";
+    field public static final String ROOM_UNNECESSARY_NULLABILITY_IN_DAO_RETURN_TYPE = "UNNECESSARY_NULLABILITY_IN_DAO_RETURN_TYPE";
   }
 
   public static final class RoomWarnings.Companion {
diff --git a/room/room-common/api/restricted_current.txt b/room/room-common/api/restricted_current.txt
index 495cba4..19b52ce 100644
--- a/room/room-common/api/restricted_current.txt
+++ b/room/room-common/api/restricted_current.txt
@@ -386,6 +386,7 @@
     field public static final String PRIMARY_KEY_FROM_EMBEDDED_IS_DROPPED = "ROOM_EMBEDDED_PRIMARY_KEY_IS_DROPPED";
     field public static final String RELATION_QUERY_WITHOUT_TRANSACTION = "ROOM_RELATION_QUERY_WITHOUT_TRANSACTION";
     field public static final String RELATION_TYPE_MISMATCH = "ROOM_RELATION_TYPE_MISMATCH";
+    field public static final String ROOM_UNNECESSARY_NULLABILITY_IN_DAO_RETURN_TYPE = "UNNECESSARY_NULLABILITY_IN_DAO_RETURN_TYPE";
   }
 
   public static final class RoomWarnings.Companion {
diff --git a/room/room-common/src/main/java/androidx/room/RoomWarnings.kt b/room/room-common/src/main/java/androidx/room/RoomWarnings.kt
index 303fe5c..c8bcfb5 100644
--- a/room/room-common/src/main/java/androidx/room/RoomWarnings.kt
+++ b/room/room-common/src/main/java/androidx/room/RoomWarnings.kt
@@ -199,6 +199,14 @@
          * Reported when there is an ambiguous column on the result of a multimap query.
          */
         public const val AMBIGUOUS_COLUMN_IN_RESULT: String = "ROOM_AMBIGUOUS_COLUMN_IN_RESULT"
+
+        /**
+         * Reported when a nullable Collection, Array or Optional is returned from a DAO method.
+         * Room will return an empty Collection, Array or Optional respectively if no results are
+         * returned by such a query, hence using a nullable return type is unnecessary in this case.
+         */
+        public const val ROOM_UNNECESSARY_NULLABILITY_IN_DAO_RETURN_TYPE: String =
+            "UNNECESSARY_NULLABILITY_IN_DAO_RETURN_TYPE"
     }
 
     @Deprecated("This type should not be instantiated as it contains only static methods. ")
diff --git a/room/room-compiler/src/main/kotlin/androidx/room/processor/ProcessorErrors.kt b/room/room-compiler/src/main/kotlin/androidx/room/processor/ProcessorErrors.kt
index b582ccf..98c9db5 100644
--- a/room/room-compiler/src/main/kotlin/androidx/room/processor/ProcessorErrors.kt
+++ b/room/room-compiler/src/main/kotlin/androidx/room/processor/ProcessorErrors.kt
@@ -1130,4 +1130,18 @@
 
     val NONNULL_VOID = "Invalid non-null declaration of 'Void', should be nullable. The 'Void' " +
         "class represents a placeholder type that is uninstantiable and 'null' is always returned."
+
+    fun nullableCollectionOrArrayReturnTypeInDaoMethod(
+        typeName: String,
+        returnType: String
+    ): String {
+        return "The nullable `$returnType` ($typeName) return type in a DAO method is " +
+        "meaningless because Room will instead return an empty `$returnType` if no rows are " +
+        "returned from the query."
+    }
+
+    fun nullableComponentInDaoMethodReturnType(typeName: String): String {
+        return "The DAO method return type ($typeName) with the nullable type argument " +
+        "is meaningless because for now Room will never put a null value in a result."
+    }
 }
\ No newline at end of file
diff --git a/room/room-compiler/src/main/kotlin/androidx/room/solver/TypeAdapterStore.kt b/room/room-compiler/src/main/kotlin/androidx/room/solver/TypeAdapterStore.kt
index 7846773..ffd31b6 100644
--- a/room/room-compiler/src/main/kotlin/androidx/room/solver/TypeAdapterStore.kt
+++ b/room/room-compiler/src/main/kotlin/androidx/room/solver/TypeAdapterStore.kt
@@ -17,6 +17,8 @@
 package androidx.room.solver
 
 import androidx.annotation.VisibleForTesting
+import androidx.room.compiler.codegen.CodeLanguage
+import androidx.room.compiler.processing.XNullability
 import androidx.room.compiler.processing.XType
 import androidx.room.compiler.processing.isArray
 import androidx.room.compiler.processing.isEnum
@@ -115,6 +117,7 @@
 import androidx.room.vo.BuiltInConverterFlags
 import androidx.room.vo.MapInfo
 import androidx.room.vo.ShortcutQueryParameter
+import androidx.room.vo.Warning
 import androidx.room.vo.isEnabled
 import com.google.common.collect.ImmutableList
 import com.google.common.collect.ImmutableListMultimap
@@ -480,6 +483,7 @@
 
         // TODO: (b/192068912) Refactor the following since this if-else cascade has gotten large
         if (typeMirror.isArray() && typeMirror.componentType.isNotByte()) {
+            checkTypeNullability(typeMirror, typeMirror.componentType, "Array")
             val rowAdapter =
                 findRowAdapter(typeMirror.componentType, query) ?: return null
             return ArrayQueryResultAdapter(typeMirror, rowAdapter)
@@ -487,6 +491,7 @@
             val rowAdapter = findRowAdapter(typeMirror, query) ?: return null
             return SingleItemQueryResultAdapter(rowAdapter)
         } else if (typeMirror.rawType.asTypeName() == GuavaTypeNames.OPTIONAL) {
+            checkTypeNullability(typeMirror, typeMirror.typeArguments.first(), "Optional")
             // Handle Guava Optional by unpacking its generic type argument and adapting that.
             // The Optional adapter will reappend the Optional type.
             val typeArg = typeMirror.typeArguments.first()
@@ -498,6 +503,8 @@
                 resultAdapter = SingleItemQueryResultAdapter(rowAdapter)
             )
         } else if (typeMirror.rawType.asTypeName() == CommonTypeNames.OPTIONAL) {
+            checkTypeNullability(typeMirror, typeMirror.typeArguments.first(), "Optional")
+
             // Handle java.util.Optional similarly.
             val typeArg = typeMirror.typeArguments.first()
             // use nullable when finding row adapter as non-null adapters might return
@@ -508,6 +515,8 @@
                 resultAdapter = SingleItemQueryResultAdapter(rowAdapter)
             )
         } else if (typeMirror.isTypeOf(ImmutableList::class)) {
+            checkTypeNullability(typeMirror, typeMirror.typeArguments.first())
+
             val typeArg = typeMirror.typeArguments.first().extendsBoundOrSelf()
             val rowAdapter = findRowAdapter(typeArg, query) ?: return null
             return ImmutableListQueryResultAdapter(
@@ -515,6 +524,8 @@
                 rowAdapter = rowAdapter
             )
         } else if (typeMirror.isTypeOf(java.util.List::class)) {
+            checkTypeNullability(typeMirror, typeMirror.typeArguments.first())
+
             val typeArg = typeMirror.typeArguments.first().extendsBoundOrSelf()
             val rowAdapter = findRowAdapter(typeArg, query) ?: return null
             return ListQueryResultAdapter(
@@ -524,6 +535,7 @@
         } else if (typeMirror.isTypeOf(ImmutableMap::class)) {
             val keyTypeArg = typeMirror.typeArguments[0].extendsBoundOrSelf()
             val valueTypeArg = typeMirror.typeArguments[1].extendsBoundOrSelf()
+            checkTypeNullability(typeMirror, keyTypeArg)
 
             // Create a type mirror for a regular Map in order to use MapQueryResultAdapter. This
             // avoids code duplication as Immutable Map can be initialized by creating an immutable
@@ -548,6 +560,7 @@
         ) {
             val keyTypeArg = typeMirror.typeArguments[0].extendsBoundOrSelf()
             val valueTypeArg = typeMirror.typeArguments[1].extendsBoundOrSelf()
+            checkTypeNullability(typeMirror, keyTypeArg)
 
             if (valueTypeArg.typeElement == null) {
                 context.logger.e(
@@ -617,6 +630,7 @@
                 else ->
                     typeMirror.typeArguments[0].extendsBoundOrSelf()
             }
+            checkTypeNullability(typeMirror, keyTypeArg)
 
             val mapValueTypeArg = if (mapType.isSparseArray()) {
                 typeMirror.typeArguments[0].extendsBoundOrSelf()
@@ -723,6 +737,35 @@
         return null
     }
 
+    private fun checkTypeNullability(
+        collectionType: XType,
+        typeArg: XType,
+        typeKeyword: String = "Collection"
+    ) {
+        if (context.codeLanguage != CodeLanguage.KOTLIN) {
+            return
+        }
+
+        if (collectionType.nullability != XNullability.NONNULL) {
+            context.logger.w(
+                Warning.UNNECESSARY_NULLABILITY_IN_DAO_RETURN_TYPE,
+                ProcessorErrors.nullableCollectionOrArrayReturnTypeInDaoMethod(
+                    collectionType.asTypeName().toString(context.codeLanguage),
+                    typeKeyword
+                )
+            )
+        }
+
+        if (typeArg.nullability != XNullability.NONNULL) {
+            context.logger.w(
+                Warning.UNNECESSARY_NULLABILITY_IN_DAO_RETURN_TYPE,
+                ProcessorErrors.nullableComponentInDaoMethodReturnType(
+                    collectionType.asTypeName().toString(context.codeLanguage)
+                )
+            )
+        }
+    }
+
     /**
      * Find a converter from cursor to the given type mirror.
      * If there is information about the query result, we try to use it to accept *any* POJO.
@@ -882,8 +925,6 @@
     }
 
     private fun getAllColumnAdapters(input: XType): List<ColumnTypeAdapter> {
-        return columnTypeAdapters.filter {
-            input.isSameType(it.out)
-        }
+        return columnTypeAdapters.filter { input.isSameType(it.out) }
     }
 }
diff --git a/room/room-compiler/src/main/kotlin/androidx/room/vo/Warning.kt b/room/room-compiler/src/main/kotlin/androidx/room/vo/Warning.kt
index df61c95..bc7b8d6 100644
--- a/room/room-compiler/src/main/kotlin/androidx/room/vo/Warning.kt
+++ b/room/room-compiler/src/main/kotlin/androidx/room/vo/Warning.kt
@@ -49,7 +49,8 @@
     EXPAND_PROJECTION_WITH_REMOVE_UNUSED_COLUMNS("ROOM_EXPAND_PROJECTION_WITH_UNUSED_COLUMNS"),
     // We shouldn't let devs suppress this error via Room so there is no runtime constant for it
     JVM_NAME_ON_OVERRIDDEN_METHOD("ROOM_JVM_NAME_IN_OVERRIDDEN_METHOD"),
-    AMBIGUOUS_COLUMN_IN_RESULT("ROOM_AMBIGUOUS_COLUMN_IN_RESULT");
+    AMBIGUOUS_COLUMN_IN_RESULT("ROOM_AMBIGUOUS_COLUMN_IN_RESULT"),
+    UNNECESSARY_NULLABILITY_IN_DAO_RETURN_TYPE("ROOM_UNNECESSARY_NULLABILITY_IN_DAO_RETURN_TYPE");
 
     companion object {
         val PUBLIC_KEY_MAP = values().associateBy { it.publicKey }
diff --git a/room/room-compiler/src/test/kotlin/androidx/room/processor/DaoProcessorTest.kt b/room/room-compiler/src/test/kotlin/androidx/room/processor/DaoProcessorTest.kt
index f1ff550..f8e5603 100644
--- a/room/room-compiler/src/test/kotlin/androidx/room/processor/DaoProcessorTest.kt
+++ b/room/room-compiler/src/test/kotlin/androidx/room/processor/DaoProcessorTest.kt
@@ -24,6 +24,8 @@
 import androidx.room.compiler.processing.util.runKspTest
 import androidx.room.compiler.processing.util.runProcessorTest
 import androidx.room.ext.RoomTypeNames.ROOM_DB
+import androidx.room.processor.ProcessorErrors.nullableComponentInDaoMethodReturnType
+import androidx.room.processor.ProcessorErrors.nullableCollectionOrArrayReturnTypeInDaoMethod
 import androidx.room.testing.context
 import androidx.room.vo.Dao
 import androidx.room.vo.ReadQueryMethod
@@ -510,6 +512,245 @@
         }
     }
 
+    @Test
+    fun testSelectQueryWithNullableCollectionReturn() {
+        val src = Source.kotlin(
+            "MyDatabase.kt",
+            """
+            import androidx.room.*
+            import com.google.common.collect.ImmutableList
+
+            @Dao
+            interface MyDao {
+              @Query("SELECT * FROM MyEntity")
+              fun nullableList(): List<MyEntity>?
+
+              @Query("SELECT * FROM MyEntity")
+              fun nullableImmutableList(): ImmutableList<MyEntity>?
+
+              @Query("SELECT * FROM MyEntity")
+              fun nullableArray(): Array<MyEntity>?
+
+              @Query("SELECT * FROM MyEntity")
+              fun nullableOptional(): java.util.Optional<MyEntity>?
+
+              @Query("SELECT * FROM MyEntity")
+              fun nullableOptionalGuava(): com.google.common.base.Optional<MyEntity>?
+
+              @Query("SELECT * FROM MyEntity JOIN MyOtherEntity ON MyEntity.pk = MyOtherEntity.otherPk")
+              fun nullableMap(): Map<MyEntity, MyOtherEntity>?
+
+              @Query("SELECT * FROM MyEntity JOIN MyOtherEntity ON MyEntity.pk = MyOtherEntity.otherPk")
+              fun nullableImmutableMap(): com.google.common.collect.ImmutableMap<MyEntity, MyOtherEntity>?
+
+              @Query("SELECT * FROM MyEntity JOIN MyOtherEntity ON MyEntity.pk = MyOtherEntity.otherPk")
+              fun nullableImmutableSetMultimap(): com.google.common.collect.ImmutableSetMultimap<MyEntity, MyOtherEntity>?
+
+              @Query("SELECT * FROM MyEntity JOIN MyOtherEntity ON MyEntity.pk = MyOtherEntity.otherPk")
+              fun nullableImmutableListMultimap(): com.google.common.collect.ImmutableListMultimap<MyEntity, MyOtherEntity>?
+            }
+
+            @Entity
+            data class MyEntity(
+                @PrimaryKey
+                var pk: Int
+            )
+
+            @Entity
+            data class MyOtherEntity(
+                @PrimaryKey
+                var otherPk: Int
+            )
+            """.trimIndent()
+        )
+        runKspTest(
+            sources = listOf(src),
+            options = mapOf(Context.BooleanProcessorOptions.GENERATE_KOTLIN.argName to "true"),
+        ) { invocation ->
+            val dao = invocation.processingEnv.requireTypeElement("MyDao")
+            val dbType = invocation.context.processingEnv.requireType(ROOM_DB)
+            DaoProcessor(
+                baseContext = invocation.context,
+                element = dao,
+                dbType = dbType,
+                dbVerifier = null
+            ).process()
+            invocation.assertCompilationResult {
+                hasWarningContaining(
+                    nullableCollectionOrArrayReturnTypeInDaoMethod(
+                        "kotlin.collections.List<MyEntity>?",
+                        "Collection"
+                    )
+                )
+                hasWarningContaining(
+                    nullableCollectionOrArrayReturnTypeInDaoMethod(
+                    "com.google.common.collect.ImmutableList<MyEntity>?",
+                        "Collection"
+                    )
+                )
+                hasWarningContaining(
+                    nullableCollectionOrArrayReturnTypeInDaoMethod(
+                        "kotlin.Array<MyEntity>?",
+                        "Array"
+                    )
+                )
+                hasWarningContaining(
+                    nullableCollectionOrArrayReturnTypeInDaoMethod(
+                        "java.util.Optional<MyEntity>?",
+                        "Optional"
+                    )
+                )
+                hasWarningContaining(
+                    nullableCollectionOrArrayReturnTypeInDaoMethod(
+                        "com.google.common.base.Optional<MyEntity>?",
+                        "Optional"
+                    )
+                )
+                hasWarningContaining(
+                    nullableCollectionOrArrayReturnTypeInDaoMethod(
+                        "kotlin.collections.Map<MyEntity, MyOtherEntity>?",
+                        "Collection"
+                    )
+                )
+                hasWarningContaining(
+                    nullableCollectionOrArrayReturnTypeInDaoMethod(
+                        "com.google.common.collect.ImmutableMap<MyEntity, MyOtherEntity>?",
+                        "Collection"
+                    )
+                )
+                hasWarningContaining(
+                    nullableCollectionOrArrayReturnTypeInDaoMethod(
+                        "com.google.common.collect.ImmutableSetMultimap<MyEntity, MyOtherEntity>?",
+                        "Collection"
+                    )
+                )
+                hasWarningContaining(
+                    nullableCollectionOrArrayReturnTypeInDaoMethod(
+                        "com.google.common.collect.ImmutableListMultimap<MyEntity, MyOtherEntity>?",
+                        "Collection"
+                    )
+                )
+                hasWarningCount(9)
+            }
+        }
+    }
+
+    @Test
+    fun testSelectQueryWithNullableTypeArgCollectionReturn() {
+        val src = Source.kotlin(
+            "MyDatabase.kt",
+            """
+            import androidx.room.*
+            import com.google.common.collect.ImmutableList
+
+            @Dao
+            interface MyDao {
+              @Query("SELECT * FROM MyEntity")
+              fun nullableList(): List<MyEntity?>
+
+              @Query("SELECT * FROM MyEntity")
+              fun nullableImmutableList(): ImmutableList<MyEntity?>
+
+              @Query("SELECT * FROM MyEntity")
+              fun nullableArray(): Array<MyEntity?>
+
+              @Query("SELECT * FROM MyEntity")
+              fun nullableOptional(): java.util.Optional<MyEntity?>
+
+              @Query("SELECT * FROM MyEntity")
+              fun nullableOptionalGuava(): com.google.common.base.Optional<MyEntity?>
+
+              @Query("SELECT * FROM MyEntity JOIN MyOtherEntity ON MyEntity.pk = MyOtherEntity.otherPk")
+              fun nullableMap(): Map<MyEntity?, MyOtherEntity>
+
+              @Query("SELECT * FROM MyEntity JOIN MyOtherEntity ON MyEntity.pk = MyOtherEntity.otherPk")
+              fun nullableImmutableMap(): com.google.common.collect.ImmutableMap<MyEntity?, MyOtherEntity>
+
+              @Query("SELECT * FROM MyEntity JOIN MyOtherEntity ON MyEntity.pk = MyOtherEntity.otherPk")
+              fun nullableImmutableSetMultimap(): com.google.common.collect.ImmutableSetMultimap<MyEntity?, MyOtherEntity>
+
+              @Query("SELECT * FROM MyEntity JOIN MyOtherEntity ON MyEntity.pk = MyOtherEntity.otherPk")
+              fun nullableImmutableListMultimap(): com.google.common.collect.ImmutableListMultimap<MyEntity?, MyOtherEntity>
+            }
+
+            @Entity
+            data class MyEntity(
+                @PrimaryKey
+                var pk: Int
+            )
+
+            @Entity
+            data class MyOtherEntity(
+                @PrimaryKey
+                var otherPk: Int
+            )
+            """.trimIndent()
+        )
+        runKspTest(
+            sources = listOf(src),
+            options = mapOf(Context.BooleanProcessorOptions.GENERATE_KOTLIN.argName to "true"),
+        ) { invocation ->
+            val dao = invocation.processingEnv.requireTypeElement("MyDao")
+            val dbType = invocation.context.processingEnv.requireType(ROOM_DB)
+            DaoProcessor(
+                baseContext = invocation.context,
+                element = dao,
+                dbType = dbType,
+                dbVerifier = null
+            ).process()
+            invocation.assertCompilationResult {
+                hasWarningContaining(
+                    nullableComponentInDaoMethodReturnType("kotlin.collections.List<MyEntity?>")
+                )
+                hasWarningContaining(
+                    nullableComponentInDaoMethodReturnType(
+                    "com.google.common.collect.ImmutableList<MyEntity?>"
+                    )
+                )
+                hasWarningContaining(
+                    nullableComponentInDaoMethodReturnType("kotlin.Array<MyEntity?>")
+                )
+                hasWarningContaining(
+                    nullableComponentInDaoMethodReturnType("java.util.Optional<MyEntity?>")
+                )
+                hasWarningContaining(
+                    nullableComponentInDaoMethodReturnType(
+                        "com.google.common.base.Optional<MyEntity?>"
+                    )
+                )
+                hasWarningContaining(
+                    nullableComponentInDaoMethodReturnType(
+                        "kotlin.collections.Map<MyEntity?, MyOtherEntity>"
+                    )
+                )
+                hasWarningContaining(
+                    nullableComponentInDaoMethodReturnType(
+                        "com.google.common.collect.ImmutableMap<MyEntity?, MyOtherEntity>"
+                    )
+                )
+                // We expect "MutableMap" when ImmutableMap is used because TypeAdapterStore will
+                // convert the map to a mutable one and re-run the `findQueryResultAdapter`
+                // algorithm
+                hasWarningContaining(
+                    nullableComponentInDaoMethodReturnType(
+                        "kotlin.collections.MutableMap<MyEntity?, MyOtherEntity>"
+                    )
+                )
+                hasWarningContaining(
+                    nullableComponentInDaoMethodReturnType(
+                        "com.google.common.collect.ImmutableSetMultimap<MyEntity?, MyOtherEntity>"
+                    )
+                )
+                hasWarningContaining(
+                    nullableComponentInDaoMethodReturnType(
+                        "com.google.common.collect.ImmutableListMultimap<MyEntity?, MyOtherEntity>"
+                    )
+                )
+                hasWarningCount(10)
+            }
+        }
+    }
+
     private fun singleDao(
         vararg inputs: String,
         classpathFiles: List<File> = emptyList(),
diff --git a/room/room-compiler/src/test/kotlin/androidx/room/writer/DaoKotlinCodeGenTest.kt b/room/room-compiler/src/test/kotlin/androidx/room/writer/DaoKotlinCodeGenTest.kt
index 3b010d8..75a5d4c 100644
--- a/room/room-compiler/src/test/kotlin/androidx/room/writer/DaoKotlinCodeGenTest.kt
+++ b/room/room-compiler/src/test/kotlin/androidx/room/writer/DaoKotlinCodeGenTest.kt
@@ -1200,15 +1200,6 @@
             interface MyDao {
               @Query("SELECT * FROM MyEntity")
               fun queryOfList(): List<MyEntity>
-
-              @Query("SELECT * FROM MyEntity")
-              fun queryOfNullableList(): List<MyEntity>?
-
-              @Query("SELECT * FROM MyEntity")
-              fun queryOfNullableEntityList(): List<MyNullableEntity?>
-
-              @Query("SELECT * FROM MyEntity")
-              fun queryOfNullableListWithNullableEntity(): List<MyNullableEntity>?
             }
 
             @Entity
@@ -1255,15 +1246,9 @@
               @Query("SELECT * FROM MyEntity")
               fun queryOfArray(): Array<MyEntity>
 
-              @Query("SELECT * FROM MyEntity")
-              fun queryOfNullableEntityArray(): Array<MyEntity?>
-
               @Query("SELECT pk FROM MyEntity")
               fun queryOfArrayWithLong(): Array<Long>
 
-              @Query("SELECT pk FROM MyEntity")
-              fun queryOfArrayWithNullLong(): Array<Long?>
-
               @Query("SELECT * FROM MyEntity")
               fun queryOfLongArray(): LongArray
 
@@ -1381,9 +1366,6 @@
             interface MyDao {
               @Query("SELECT * FROM MyEntity")
               fun queryOfList(): ImmutableList<MyEntity>
-
-              @Query("SELECT * FROM MyEntity")
-              fun queryOfNullableEntityList(): ImmutableList<MyEntity?>
             }
 
             @Entity
@@ -1894,9 +1876,6 @@
 
                 @Query("SELECT * FROM MyEntity WHERE pk IN (:arg)")
                 suspend fun getSuspendList(vararg arg: String?): List<MyEntity>
-
-                @Query("SELECT * FROM MyEntity WHERE pk IN (:arg)")
-                suspend fun getNullableSuspendList(vararg arg: String?): List<MyEntity?>
             }
 
             @Entity
diff --git a/room/room-compiler/src/test/test-data/kotlinCodeGen/coroutines.kt b/room/room-compiler/src/test/test-data/kotlinCodeGen/coroutines.kt
index 320f6b2..0a6dff1 100644
--- a/room/room-compiler/src/test/test-data/kotlinCodeGen/coroutines.kt
+++ b/room/room-compiler/src/test/test-data/kotlinCodeGen/coroutines.kt
@@ -172,50 +172,6 @@
         })
     }
 
-    public override suspend fun getNullableSuspendList(arg: Array<out String?>): List<MyEntity?> {
-        val _stringBuilder: StringBuilder = newStringBuilder()
-        _stringBuilder.append("SELECT * FROM MyEntity WHERE pk IN (")
-        val _inputSize: Int = arg.size
-        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)
-            }
-            _argIndex++
-        }
-        val _cancellationSignal: CancellationSignal? = createCancellationSignal()
-        return execute(__db, false, _cancellationSignal, object : Callable<List<MyEntity?>> {
-            public override fun call(): List<MyEntity?> {
-                val _cursor: Cursor = query(__db, _statement, false, null)
-                try {
-                    val _cursorIndexOfPk: Int = getColumnIndexOrThrow(_cursor, "pk")
-                    val _cursorIndexOfOther: Int = getColumnIndexOrThrow(_cursor, "other")
-                    val _result: MutableList<MyEntity?> = ArrayList<MyEntity?>(_cursor.getCount())
-                    while (_cursor.moveToNext()) {
-                        val _item_1: MyEntity?
-                        val _tmpPk: Int
-                        _tmpPk = _cursor.getInt(_cursorIndexOfPk)
-                        val _tmpOther: String
-                        _tmpOther = _cursor.getString(_cursorIndexOfOther)
-                        _item_1 = MyEntity(_tmpPk,_tmpOther)
-                        _result.add(_item_1)
-                    }
-                    return _result
-                } finally {
-                    _cursor.close()
-                    _statement.release()
-                }
-            }
-        })
-    }
-
     public companion object {
         @JvmStatic
         public fun getRequiredConverters(): List<Class<*>> = emptyList()
diff --git a/room/room-compiler/src/test/test-data/kotlinCodeGen/queryResultAdapter_array.kt b/room/room-compiler/src/test/test-data/kotlinCodeGen/queryResultAdapter_array.kt
index 1609b43..bcafeea 100644
--- a/room/room-compiler/src/test/test-data/kotlinCodeGen/queryResultAdapter_array.kt
+++ b/room/room-compiler/src/test/test-data/kotlinCodeGen/queryResultAdapter_array.kt
@@ -59,37 +59,6 @@
         }
     }
 
-    public override fun queryOfNullableEntityArray(): Array<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 _cursorIndexOfOther: Int = getColumnIndexOrThrow(_cursor, "other")
-            val _cursorIndexOfOther2: Int = getColumnIndexOrThrow(_cursor, "other2")
-            val _tmpResult: Array<MyEntity?> = arrayOfNulls<MyEntity?>(_cursor.getCount())
-            var _index: Int = 0
-            while (_cursor.moveToNext()) {
-                val _item: MyEntity?
-                val _tmpPk: Int
-                _tmpPk = _cursor.getInt(_cursorIndexOfPk)
-                val _tmpOther: String
-                _tmpOther = _cursor.getString(_cursorIndexOfOther)
-                val _tmpOther2: Long
-                _tmpOther2 = _cursor.getLong(_cursorIndexOfOther2)
-                _item = MyEntity(_tmpPk,_tmpOther,_tmpOther2)
-                _tmpResult[_index] = _item
-                _index++
-            }
-            val _result: Array<MyEntity?> = _tmpResult
-            return _result
-        } finally {
-            _cursor.close()
-            _statement.release()
-        }
-    }
-
     public override fun queryOfArrayWithLong(): Array<Long> {
         val _sql: String = "SELECT pk FROM MyEntity"
         val _statement: RoomSQLiteQuery = acquire(_sql, 0)
@@ -112,32 +81,6 @@
         }
     }
 
-    public override fun queryOfArrayWithNullLong(): Array<Long?> {
-        val _sql: String = "SELECT pk FROM MyEntity"
-        val _statement: RoomSQLiteQuery = acquire(_sql, 0)
-        __db.assertNotSuspendingTransaction()
-        val _cursor: Cursor = query(__db, _statement, false, null)
-        try {
-            val _tmpResult: Array<Long?> = arrayOfNulls<Long?>(_cursor.getCount())
-            var _index: Int = 0
-            while (_cursor.moveToNext()) {
-                val _item: Long?
-                if (_cursor.isNull(0)) {
-                    _item = null
-                } else {
-                    _item = _cursor.getLong(0)
-                }
-                _tmpResult[_index] = _item
-                _index++
-            }
-            val _result: Array<Long?> = _tmpResult
-            return _result
-        } finally {
-            _cursor.close()
-            _statement.release()
-        }
-    }
-
     public override fun queryOfLongArray(): LongArray {
         val _sql: String = "SELECT * FROM MyEntity"
         val _statement: RoomSQLiteQuery = acquire(_sql, 0)
diff --git a/room/room-compiler/src/test/test-data/kotlinCodeGen/queryResultAdapter_immutable_list.kt b/room/room-compiler/src/test/test-data/kotlinCodeGen/queryResultAdapter_immutable_list.kt
index 9a02399..c0118e4 100644
--- a/room/room-compiler/src/test/test-data/kotlinCodeGen/queryResultAdapter_immutable_list.kt
+++ b/room/room-compiler/src/test/test-data/kotlinCodeGen/queryResultAdapter_immutable_list.kt
@@ -49,32 +49,6 @@
         }
     }
 
-    public override fun queryOfNullableEntityList(): ImmutableList<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 _cursorIndexOfOther: Int = getColumnIndexOrThrow(_cursor, "other")
-            val _immutableListBuilder: ImmutableList.Builder<MyEntity?> = ImmutableList.builder()
-            while (_cursor.moveToNext()) {
-                val _item: MyEntity?
-                val _tmpPk: Int
-                _tmpPk = _cursor.getInt(_cursorIndexOfPk)
-                val _tmpOther: String
-                _tmpOther = _cursor.getString(_cursorIndexOfOther)
-                _item = MyEntity(_tmpPk,_tmpOther)
-                _immutableListBuilder.add(_item)
-            }
-            val _result: ImmutableList<MyEntity?> = _immutableListBuilder.build()
-            return _result
-        } finally {
-            _cursor.close()
-            _statement.release()
-        }
-    }
-
     public companion object {
         @JvmStatic
         public fun getRequiredConverters(): List<Class<*>> = emptyList()
diff --git a/room/room-compiler/src/test/test-data/kotlinCodeGen/queryResultAdapter_list.kt b/room/room-compiler/src/test/test-data/kotlinCodeGen/queryResultAdapter_list.kt
index db92cd8..b1f3933 100644
--- a/room/room-compiler/src/test/test-data/kotlinCodeGen/queryResultAdapter_list.kt
+++ b/room/room-compiler/src/test/test-data/kotlinCodeGen/queryResultAdapter_list.kt
@@ -49,97 +49,6 @@
         }
     }
 
-    public override fun queryOfNullableList(): List<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 _cursorIndexOfOther: Int = getColumnIndexOrThrow(_cursor, "other")
-            val _result: MutableList<MyEntity> = ArrayList<MyEntity>(_cursor.getCount())
-            while (_cursor.moveToNext()) {
-                val _item: MyEntity
-                val _tmpPk: Int
-                _tmpPk = _cursor.getInt(_cursorIndexOfPk)
-                val _tmpOther: String
-                _tmpOther = _cursor.getString(_cursorIndexOfOther)
-                _item = MyEntity(_tmpPk,_tmpOther)
-                _result.add(_item)
-            }
-            return _result
-        } finally {
-            _cursor.close()
-            _statement.release()
-        }
-    }
-
-    public override fun queryOfNullableEntityList(): List<MyNullableEntity?> {
-        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 _cursorIndexOfOther: Int = getColumnIndexOrThrow(_cursor, "other")
-            val _result: MutableList<MyNullableEntity?> = ArrayList<MyNullableEntity?>(_cursor.getCount())
-            while (_cursor.moveToNext()) {
-                val _item: MyNullableEntity?
-                val _tmpPk: Int?
-                if (_cursor.isNull(_cursorIndexOfPk)) {
-                    _tmpPk = null
-                } else {
-                    _tmpPk = _cursor.getInt(_cursorIndexOfPk)
-                }
-                val _tmpOther: String?
-                if (_cursor.isNull(_cursorIndexOfOther)) {
-                    _tmpOther = null
-                } else {
-                    _tmpOther = _cursor.getString(_cursorIndexOfOther)
-                }
-                _item = MyNullableEntity(_tmpPk,_tmpOther)
-                _result.add(_item)
-            }
-            return _result
-        } finally {
-            _cursor.close()
-            _statement.release()
-        }
-    }
-
-    public override fun queryOfNullableListWithNullableEntity(): List<MyNullableEntity>? {
-        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 _cursorIndexOfOther: Int = getColumnIndexOrThrow(_cursor, "other")
-            val _result: MutableList<MyNullableEntity> = ArrayList<MyNullableEntity>(_cursor.getCount())
-            while (_cursor.moveToNext()) {
-                val _item: MyNullableEntity
-                val _tmpPk: Int?
-                if (_cursor.isNull(_cursorIndexOfPk)) {
-                    _tmpPk = null
-                } else {
-                    _tmpPk = _cursor.getInt(_cursorIndexOfPk)
-                }
-                val _tmpOther: String?
-                if (_cursor.isNull(_cursorIndexOfOther)) {
-                    _tmpOther = null
-                } else {
-                    _tmpOther = _cursor.getString(_cursorIndexOfOther)
-                }
-                _item = MyNullableEntity(_tmpPk,_tmpOther)
-                _result.add(_item)
-            }
-            return _result
-        } finally {
-            _cursor.close()
-            _statement.release()
-        }
-    }
-
     public companion object {
         @JvmStatic
         public fun getRequiredConverters(): List<Class<*>> = emptyList()
diff --git a/samples/MediaRoutingDemo/build.gradle b/samples/MediaRoutingDemo/build.gradle
index 9f763ae..5ec869d 100644
--- a/samples/MediaRoutingDemo/build.gradle
+++ b/samples/MediaRoutingDemo/build.gradle
@@ -8,6 +8,7 @@
     implementation(project(":mediarouter:mediarouter"))
     implementation(project(":recyclerview:recyclerview"))
     implementation("androidx.concurrent:concurrent-futures:1.1.0")
+    implementation(libs.material)
 }
 
 android {
diff --git a/samples/MediaRoutingDemo/src/main/AndroidManifest.xml b/samples/MediaRoutingDemo/src/main/AndroidManifest.xml
index f98ad81..0c06dfa 100644
--- a/samples/MediaRoutingDemo/src/main/AndroidManifest.xml
+++ b/samples/MediaRoutingDemo/src/main/AndroidManifest.xml
@@ -52,6 +52,17 @@
             </intent-filter>
         </activity>
 
+        <activity
+            android:name=".activities.AddEditRouteActivity"
+            android:configChanges="orientation|screenSize"
+            android:exported="false"
+            android:label="@string/sample_media_router_activity_dark">
+            <intent-filter>
+                <action android:name="android.intent.action.MAIN" />
+                <category android:name="com.example.androidx.SAMPLE_CODE" />
+            </intent-filter>
+        </activity>
+
         <receiver android:name="androidx.mediarouter.media.MediaTransferReceiver"
             android:exported="true" />
 
@@ -99,5 +110,4 @@
     <!-- Permission for INTERNET is required for streaming video content
          from the web, it's not required otherwise. -->
     <uses-permission android:name="android.permission.INTERNET" />
-
 </manifest>
diff --git a/samples/MediaRoutingDemo/src/main/java/com/example/androidx/mediarouting/RoutesManager.java b/samples/MediaRoutingDemo/src/main/java/com/example/androidx/mediarouting/RoutesManager.java
index 3113ac0..99934de 100644
--- a/samples/MediaRoutingDemo/src/main/java/com/example/androidx/mediarouting/RoutesManager.java
+++ b/samples/MediaRoutingDemo/src/main/java/com/example/androidx/mediarouting/RoutesManager.java
@@ -28,6 +28,8 @@
 
 import androidx.annotation.NonNull;
 import androidx.annotation.Nullable;
+import androidx.mediarouter.media.MediaRouter;
+import androidx.mediarouter.media.MediaRouterParams;
 
 import com.example.androidx.mediarouting.data.RouteItem;
 
@@ -44,6 +46,7 @@
     private static final int VOLUME_DEFAULT = 5;
 
     private boolean mDynamicRoutingEnabled;
+    private DialogType mDialogType;
 
     private Context mContext;
     private final Map<String, RouteItem> mRouteItems;
@@ -52,6 +55,7 @@
     private RoutesManager(Context context) {
         mContext = context;
         mDynamicRoutingEnabled = true;
+        mDialogType = DialogType.OUTPUT_SWITCHER;
         mRouteItems = new HashMap<>();
         initTestRoutes();
     }
@@ -80,6 +84,10 @@
         this.mDynamicRoutingEnabled = dynamicRoutingEnabled;
     }
 
+    public void setDialogType(@NonNull DialogType dialogType) {
+        this.mDialogType = dialogType;
+    }
+
     /**
      * Deletes the route with the passed id.
      *
@@ -101,10 +109,33 @@
     }
 
     /** Adds the given route to the manager, replacing any existing route with a matching id. */
-    public void addOrUpdateRoute(@NonNull RouteItem routeItem) {
+    public void addRoute(@NonNull RouteItem routeItem) {
         mRouteItems.put(routeItem.getId(), routeItem);
     }
 
+    /** Changes the media router dialog type with the type stored in {@link RoutesManager} */
+    public void reloadDialogType() {
+        MediaRouter mediaRouter = MediaRouter.getInstance(mContext.getApplicationContext());
+        MediaRouterParams.Builder builder =
+                new MediaRouterParams.Builder(mediaRouter.getRouterParams());
+        switch (mDialogType) {
+            case DEFAULT:
+                builder.setDialogType(MediaRouterParams.DIALOG_TYPE_DEFAULT)
+                        .setOutputSwitcherEnabled(false);
+                mediaRouter.setRouterParams(builder.build());
+                break;
+            case DYNAMIC_GROUP:
+                builder.setDialogType(MediaRouterParams.DIALOG_TYPE_DYNAMIC_GROUP)
+                        .setOutputSwitcherEnabled(false);
+                mediaRouter.setRouterParams(builder.build());
+                break;
+            case OUTPUT_SWITCHER:
+                builder.setOutputSwitcherEnabled(true);
+                mediaRouter.setRouterParams(builder.build());
+                break;
+        }
+    }
+
     private void initTestRoutes() {
         Resources r = mContext.getResources();
 
@@ -165,4 +196,10 @@
         mRouteItems.put(r3.getId(), r3);
         mRouteItems.put(r4.getId(), r4);
     }
+
+    public enum DialogType {
+        DEFAULT,
+        DYNAMIC_GROUP,
+        OUTPUT_SWITCHER
+    }
 }
diff --git a/samples/MediaRoutingDemo/src/main/java/com/example/androidx/mediarouting/activities/AddEditRouteActivity.java b/samples/MediaRoutingDemo/src/main/java/com/example/androidx/mediarouting/activities/AddEditRouteActivity.java
new file mode 100644
index 0000000..09d7eed
--- /dev/null
+++ b/samples/MediaRoutingDemo/src/main/java/com/example/androidx/mediarouting/activities/AddEditRouteActivity.java
@@ -0,0 +1,246 @@
+/*
+ * 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 com.example.androidx.mediarouting.activities;
+
+import android.content.ComponentName;
+import android.content.Context;
+import android.content.Intent;
+import android.content.ServiceConnection;
+import android.os.Bundle;
+import android.os.IBinder;
+import android.text.Editable;
+import android.text.TextWatcher;
+import android.view.View;
+import android.widget.AdapterView;
+import android.widget.ArrayAdapter;
+import android.widget.Button;
+import android.widget.EditText;
+import android.widget.Spinner;
+import android.widget.Switch;
+
+import androidx.annotation.NonNull;
+import androidx.annotation.Nullable;
+import androidx.appcompat.app.AppCompatActivity;
+
+import com.example.androidx.mediarouting.R;
+import com.example.androidx.mediarouting.RoutesManager;
+import com.example.androidx.mediarouting.data.RouteItem;
+import com.example.androidx.mediarouting.services.SampleDynamicGroupMediaRouteProviderService;
+
+/** Allows the user to add and edit routes. */
+public class AddEditRouteActivity extends AppCompatActivity {
+
+    private static final String EXTRA_ROUTE_ID_KEY = "routeId";
+
+    private SampleDynamicGroupMediaRouteProviderService mService;
+    private ServiceConnection mConnection;
+    private RoutesManager mRoutesManager;
+    private RouteItem mRouteItem;
+    private Switch mCanDisconnectSwitch;
+
+    /** Launches the activity. */
+    public static void launchActivity(@NonNull Context context, @Nullable String routeId) {
+        Intent intent = new Intent(context, AddEditRouteActivity.class);
+        intent.putExtra(EXTRA_ROUTE_ID_KEY, routeId);
+        context.startActivity(intent);
+    }
+
+    @Override
+    protected void onCreate(@Nullable Bundle savedInstanceState) {
+        super.onCreate(savedInstanceState);
+        setContentView(R.layout.activity_add_edit_route);
+
+        mRoutesManager = RoutesManager.getInstance(getApplicationContext());
+
+        String routeId = getIntent().getStringExtra(EXTRA_ROUTE_ID_KEY);
+        mRouteItem = mRoutesManager.getRouteWithId(routeId);
+        mConnection = new ProviderServiceConnection();
+
+        if (mRouteItem == null) {
+            mRouteItem = new RouteItem();
+        } else {
+            mRouteItem = RouteItem.copyOf(mRouteItem);
+        }
+
+        mCanDisconnectSwitch = findViewById(R.id.cam_disconnect_switch);
+
+        setUpViews();
+    }
+
+    @Override
+    protected void onStart() {
+        super.onStart();
+        Intent intent = new Intent(this, SampleDynamicGroupMediaRouteProviderService.class);
+        intent.setAction(SampleDynamicGroupMediaRouteProviderService.ACTION_BIND_LOCAL);
+        bindService(intent, mConnection, Context.BIND_AUTO_CREATE);
+    }
+
+    @Override
+    protected void onStop() {
+        super.onStop();
+        unbindService(mConnection);
+    }
+
+    private void setUpViews() {
+        setUpEditText(
+                findViewById(R.id.name_edit_text),
+                mRouteItem.getName(),
+                newName -> mRouteItem.setName(newName));
+
+        setUpEditText(
+                findViewById(R.id.description_edit_text),
+                mRouteItem.getDescription(),
+                newDescription -> mRouteItem.setDescription(newDescription));
+
+        setUpEnumBasedSpinner(
+                /* context= */ this,
+                RouteItem.ControlFilter.values(),
+                findViewById(R.id.control_filters_spinner),
+                mRouteItem.getControlFilter(),
+                newControlFilter ->
+                        mRouteItem.setControlFilter((RouteItem.ControlFilter) newControlFilter));
+
+        setUpEnumBasedSpinner(
+                /* context= */ this,
+                RouteItem.PlaybackStream.values(),
+                findViewById(R.id.playback_stream_spinner),
+                mRouteItem.getPlaybackStream(),
+                newPlaybackStream ->
+                        mRouteItem.setPlaybackStream((RouteItem.PlaybackStream) newPlaybackStream));
+
+        setUpEnumBasedSpinner(
+                /* context= */ this,
+                RouteItem.PlaybackType.values(),
+                findViewById(R.id.playback_type_spinner),
+                mRouteItem.getPlaybackType(),
+                newPlaybackType ->
+                        mRouteItem.setPlaybackType((RouteItem.PlaybackType) newPlaybackType));
+
+        setUpEnumBasedSpinner(
+                /* context= */ this,
+                RouteItem.DeviceType.values(),
+                findViewById(R.id.device_type_spinner),
+                mRouteItem.getDeviceType(),
+                newDeviceType -> mRouteItem.setDeviceType((RouteItem.DeviceType) newDeviceType));
+
+        setUpEnumBasedSpinner(
+                /* context= */ this,
+                RouteItem.VolumeHandling.values(),
+                findViewById(R.id.volume_handling_spinner),
+                mRouteItem.getVolumeHandling(),
+                mewVolumeHandling ->
+                        mRouteItem.setVolumeHandling((RouteItem.VolumeHandling) mewVolumeHandling));
+
+        setUpEditText(
+                findViewById(R.id.volume_edit_text),
+                String.valueOf(mRouteItem.getVolume()),
+                mewVolume -> mRouteItem.setVolume(Integer.parseInt(mewVolume)));
+
+        setUpEditText(
+                findViewById(R.id.volume_max_edit_text),
+                String.valueOf(mRouteItem.getVolumeMax()),
+                mewVolumeMax -> mRouteItem.setVolumeMax(Integer.parseInt(mewVolumeMax)));
+
+        setUpCanDisconnectSwitch();
+
+        setUpSaveButton();
+    }
+
+    private void setUpCanDisconnectSwitch() {
+        mCanDisconnectSwitch.setChecked(mRouteItem.isCanDisconnect());
+        mCanDisconnectSwitch.setOnCheckedChangeListener(
+                (compoundButton, b) -> {
+                    mRouteItem.setCanDisconnect(b);
+                });
+    }
+
+    private void setUpSaveButton() {
+        Button saveButton = findViewById(R.id.save_button);
+        saveButton.setOnClickListener(
+                view -> {
+                    mRoutesManager.addRoute(mRouteItem);
+                    mService.reloadRoutes();
+                    finish();
+                });
+    }
+
+    private static void setUpEditText(
+            EditText editText,
+            String currentValue,
+            RoutePropertySetter<String> routePropertySetter) {
+        editText.setText(currentValue);
+        editText.addTextChangedListener(
+                new TextWatcher() {
+                    @Override
+                    public void beforeTextChanged(
+                            CharSequence charSequence, int i, int i1, int i2) {}
+
+                    @Override
+                    public void onTextChanged(CharSequence charSequence, int i, int i1, int i2) {
+                        routePropertySetter.accept(charSequence.toString());
+                    }
+
+                    @Override
+                    public void afterTextChanged(Editable editable) {}
+                });
+    }
+
+    private static void setUpEnumBasedSpinner(
+            Context context,
+            Enum<?>[] values,
+            Spinner spinner,
+            Enum<?> anEnum,
+            RoutePropertySetter<Enum<?>> routePropertySetter) {
+        ArrayAdapter<Enum<?>> adapter =
+                new ArrayAdapter<>(context, android.R.layout.simple_spinner_item, values);
+        adapter.setDropDownViewResource(android.R.layout.simple_spinner_dropdown_item);
+        spinner.setAdapter(adapter);
+        spinner.setSelection(anEnum.ordinal());
+
+        spinner.setOnItemSelectedListener(
+                new AdapterView.OnItemSelectedListener() {
+                    @Override
+                    public void onItemSelected(
+                            AdapterView<?> adapterView, View view, int i, long l) {
+                        routePropertySetter.accept(
+                                anEnum.getDeclaringClass().getEnumConstants()[i]);
+                    }
+
+                    @Override
+                    public void onNothingSelected(AdapterView<?> adapterView) {}
+                });
+    }
+
+    private interface RoutePropertySetter<T> {
+        void accept(T value);
+    }
+
+    private class ProviderServiceConnection implements ServiceConnection {
+
+        @Override
+        public void onServiceConnected(ComponentName className, IBinder service) {
+            SampleDynamicGroupMediaRouteProviderService.LocalBinder binder =
+                    (SampleDynamicGroupMediaRouteProviderService.LocalBinder) service;
+            mService = binder.getService();
+        }
+
+        @Override
+        public void onServiceDisconnected(ComponentName arg0) {
+            mService = null;
+        }
+    }
+}
diff --git a/samples/MediaRoutingDemo/src/main/java/com/example/androidx/mediarouting/activities/MainActivity.java b/samples/MediaRoutingDemo/src/main/java/com/example/androidx/mediarouting/activities/MainActivity.java
index 2f4f362..8117b44 100644
--- a/samples/MediaRoutingDemo/src/main/java/com/example/androidx/mediarouting/activities/MainActivity.java
+++ b/samples/MediaRoutingDemo/src/main/java/com/example/androidx/mediarouting/activities/MainActivity.java
@@ -16,11 +16,14 @@
 
 package com.example.androidx.mediarouting.activities;
 
+import android.Manifest;
+import android.app.Activity;
 import android.app.PendingIntent;
 import android.content.BroadcastReceiver;
 import android.content.ComponentName;
 import android.content.Context;
 import android.content.Intent;
+import android.content.pm.PackageManager;
 import android.net.Uri;
 import android.os.Build;
 import android.os.Bundle;
@@ -49,6 +52,7 @@
 import androidx.annotation.RequiresApi;
 import androidx.appcompat.app.AppCompatActivity;
 import androidx.concurrent.futures.CallbackToFutureAdapter;
+import androidx.core.content.ContextCompat;
 import androidx.core.view.MenuItemCompat;
 import androidx.fragment.app.FragmentManager;
 import androidx.mediarouter.app.MediaRouteActionProvider;
@@ -66,6 +70,7 @@
 
 import com.example.androidx.mediarouting.MyMediaRouteControllerDialog;
 import com.example.androidx.mediarouting.R;
+import com.example.androidx.mediarouting.RoutesManager;
 import com.example.androidx.mediarouting.data.MediaItem;
 import com.example.androidx.mediarouting.data.PlaylistItem;
 import com.example.androidx.mediarouting.player.Player;
@@ -83,9 +88,11 @@
 public class MainActivity extends AppCompatActivity {
     private static final String TAG = "MainActivity";
     private static final String DISCOVERY_FRAGMENT_TAG = "DiscoveryFragment";
+    private static final int POST_NOTIFICATIONS_PERMISSION_REQUEST_CODE = 5001;
     private static final boolean ENABLE_DEFAULT_CONTROL_CHECK_BOX = false;
 
     private MediaRouter mMediaRouter;
+    private RoutesManager mRoutesManager;
     private MediaRouteSelector mSelector;
     private PlaylistAdapter mPlayListItems;
     private TextView mInfoTextView;
@@ -96,14 +103,15 @@
 
     final Handler mHandler = new Handler();
 
-    private final Runnable mUpdateSeekRunnable = new Runnable() {
-        @Override
-        public void run() {
-            updateProgress();
-            // update Ui every 1 second
-            mHandler.postDelayed(this, 1000);
-        }
-    };
+    private final Runnable mUpdateSeekRunnable =
+            new Runnable() {
+                @Override
+                public void run() {
+                    updateProgress();
+                    // update Ui every 1 second
+                    mHandler.postDelayed(this, 1000);
+                }
+            };
 
     final SessionManager mSessionManager = new SessionManager("app");
     Player mPlayer;
@@ -204,21 +212,16 @@
 
     @Override
     protected void onCreate(@Nullable Bundle savedInstanceState) {
-        // Be sure to call the super class.
         super.onCreate(savedInstanceState);
 
-        // Need overlay permission for emulating remote display.
-        if (Build.VERSION.SDK_INT >= 23 && !Api23Impl.canDrawOverlays(this)) {
-            Intent intent = new Intent(Settings.ACTION_MANAGE_OVERLAY_PERMISSION,
-                    Uri.parse("package:" + getPackageName()));
-            startActivityForResult(intent, 0);
-        }
+        requestRequiredPermissions();
 
-        // Get the media router service.
         mMediaRouter = MediaRouter.getInstance(this);
-
         mMediaRouter.setRouterParams(getRouterParams());
 
+        mRoutesManager = RoutesManager.getInstance(getApplicationContext());
+        mRoutesManager.reloadDialogType();
+
         // Create a route selector for the type of routes that we care about.
         mSelector = new MediaRouteSelector.Builder()
                 .addControlCategory(MediaControlIntent.CATEGORY_REMOTE_PLAYBACK)
@@ -363,8 +366,12 @@
                 SampleMediaButtonReceiver.class.getName());
         Intent mediaButtonIntent = new Intent(Intent.ACTION_MEDIA_BUTTON);
         mediaButtonIntent.setComponent(mEventReceiver);
-        mMediaPendingIntent = PendingIntent.getBroadcast(this, /* requestCode = */0,
-                mediaButtonIntent, PendingIntent.FLAG_IMMUTABLE);
+        mMediaPendingIntent =
+                PendingIntent.getBroadcast(
+                        this,
+                        /* requestCode= */ 0,
+                        mediaButtonIntent,
+                        PendingIntent.FLAG_IMMUTABLE);
 
         // Create and register the remote control client
         createMediaSession();
@@ -389,6 +396,40 @@
         updateUi();
     }
 
+    private void requestRequiredPermissions() {
+        requestDisplayOverOtherAppsPermission();
+        requestPostNotificationsPermission();
+    }
+
+    private void requestDisplayOverOtherAppsPermission() {
+        // Need overlay permission for emulating remote display.
+        if (Build.VERSION.SDK_INT >= 23 && !Api23Impl.canDrawOverlays(this)) {
+            Intent intent =
+                    new Intent(
+                            Settings.ACTION_MANAGE_OVERLAY_PERMISSION,
+                            Uri.parse("package:" + getPackageName()));
+            startActivityForResult(intent, 0);
+        }
+    }
+
+    private void requestPostNotificationsPermission() {
+        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
+            if (ContextCompat.checkSelfPermission(
+                            getApplicationContext(), Manifest.permission.POST_NOTIFICATIONS)
+                    != PackageManager.PERMISSION_GRANTED) {
+                if (!Api23Impl.shouldShowRequestPermissionRationale(
+                        this, Manifest.permission.POST_NOTIFICATIONS)) {
+                    Api23Impl.requestPermissions(
+                            /* activity= */ this,
+                            /* permissions= */ new String[] {
+                                Manifest.permission.POST_NOTIFICATIONS
+                            },
+                            /* requestCode= */ POST_NOTIFICATIONS_PERMISSION_REQUEST_CODE);
+                }
+            }
+        }
+    }
+
     private void createMediaSession() {
         // Create the MediaSession
         mMediaSession = new MediaSessionCompat(this, "SampleMediaRouter", mEventReceiver,
@@ -708,5 +749,15 @@
         static boolean canDrawOverlays(Context context) {
             return Settings.canDrawOverlays(context);
         }
+
+        @DoNotInline
+        static boolean shouldShowRequestPermissionRationale(Activity activity, String permission) {
+            return activity.shouldShowRequestPermissionRationale(permission);
+        }
+
+        @DoNotInline
+        static void requestPermissions(Activity activity, String[] permissions, int requestCode) {
+            activity.requestPermissions(permissions, requestCode);
+        }
     }
 }
diff --git a/samples/MediaRoutingDemo/src/main/java/com/example/androidx/mediarouting/activities/SettingsActivity.java b/samples/MediaRoutingDemo/src/main/java/com/example/androidx/mediarouting/activities/SettingsActivity.java
index 4acd47a..ade6898 100644
--- a/samples/MediaRoutingDemo/src/main/java/com/example/androidx/mediarouting/activities/SettingsActivity.java
+++ b/samples/MediaRoutingDemo/src/main/java/com/example/androidx/mediarouting/activities/SettingsActivity.java
@@ -41,6 +41,7 @@
 import com.example.androidx.mediarouting.RoutesManager;
 import com.example.androidx.mediarouting.services.SampleDynamicGroupMediaRouteProviderService;
 import com.example.androidx.mediarouting.ui.RoutesAdapter;
+import com.google.android.material.floatingactionbutton.FloatingActionButton;
 
 /**
  * Allows the user to control dialog types, enabling or disabling Dynamic Groups, enabling or
@@ -66,15 +67,14 @@
 
         mConnection = new ProviderServiceConnection();
 
-        setUpDynamicGroupsEnabledSwitch();
-        setUpTransferToLocalSwitch();
-        setUpDialogTypeDropDownList();
+        setUpViews();
 
         mRouteItemListener =
                 new RoutesAdapter.RouteItemListener() {
                     @Override
                     public void onRouteEditClick(@NonNull String routeId) {
-                        // TODO: Navigate to a new editing screen in a different CL
+                        AddEditRouteActivity.launchActivity(
+                                /* context= */ SettingsActivity.this, /* routeId */ routeId);
                     }
 
                     @Override
@@ -96,9 +96,10 @@
                 };
 
         RecyclerView routeList = findViewById(R.id.routes_recycler_view);
-        routeList.setLayoutManager(new LinearLayoutManager(/* context */ this));
+        routeList.setLayoutManager(new LinearLayoutManager(/* context= */ this));
         mRoutesAdapter = new RoutesAdapter(mRoutesManager.getRouteItems(), mRouteItemListener);
         routeList.setAdapter(mRoutesAdapter);
+        routeList.setHasFixedSize(true);
     }
 
     @Override
@@ -122,6 +123,13 @@
         unbindService(mConnection);
     }
 
+    private void setUpViews() {
+        setUpDynamicGroupsEnabledSwitch();
+        setUpTransferToLocalSwitch();
+        setUpDialogTypeDropDownList();
+        setUpNewRouteButton();
+    }
+
     private void setUpDynamicGroupsEnabledSwitch() {
         Switch dynamicRoutingEnabled = findViewById(R.id.dynamic_routing_switch);
         dynamicRoutingEnabled.setChecked(mRoutesManager.isDynamicRoutingEnabled());
@@ -151,20 +159,8 @@
                     @Override
                     public void onItemSelected(
                             AdapterView<?> adapterView, View view, int i, long l) {
-                        MediaRouterParams.Builder builder =
-                                new MediaRouterParams.Builder(mMediaRouter.getRouterParams());
-                        if (i == 0) {
-                            builder.setDialogType(MediaRouterParams.DIALOG_TYPE_DEFAULT)
-                                    .setOutputSwitcherEnabled(false);
-                            mMediaRouter.setRouterParams(builder.build());
-                        } else if (i == 1) {
-                            builder.setDialogType(MediaRouterParams.DIALOG_TYPE_DYNAMIC_GROUP)
-                                    .setOutputSwitcherEnabled(false);
-                            mMediaRouter.setRouterParams(builder.build());
-                        } else if (i == 2) {
-                            builder.setOutputSwitcherEnabled(true);
-                            mMediaRouter.setRouterParams(builder.build());
-                        }
+                        mRoutesManager.setDialogType(RoutesManager.DialogType.values()[i]);
+                        mRoutesManager.reloadDialogType();
                     }
 
                     @Override
@@ -187,6 +183,15 @@
         }
     }
 
+    private void setUpNewRouteButton() {
+        FloatingActionButton newRouteButton = findViewById(R.id.new_route_button);
+        newRouteButton.setOnClickListener(
+                view -> {
+                    AddEditRouteActivity.launchActivity(
+                            /* context= */ SettingsActivity.this, /* routeId= */ null);
+                });
+    }
+
     private class ProviderServiceConnection implements ServiceConnection {
 
         @Override
diff --git a/samples/MediaRoutingDemo/src/main/java/com/example/androidx/mediarouting/data/RouteItem.java b/samples/MediaRoutingDemo/src/main/java/com/example/androidx/mediarouting/data/RouteItem.java
index 7e80d86..3f13bc9 100644
--- a/samples/MediaRoutingDemo/src/main/java/com/example/androidx/mediarouting/data/RouteItem.java
+++ b/samples/MediaRoutingDemo/src/main/java/com/example/androidx/mediarouting/data/RouteItem.java
@@ -53,6 +53,51 @@
         this.mGroupMemberIds = new ArrayList<>();
     }
 
+    public RouteItem(
+            @NonNull String id,
+            @NonNull String name,
+            @NonNull String description,
+            @NonNull ControlFilter controlFilter,
+            @NonNull PlaybackStream playbackStream,
+            @NonNull PlaybackType playbackType,
+            boolean canDisconnect,
+            @NonNull VolumeHandling volumeHandling,
+            int volume,
+            int volumeMax,
+            @NonNull DeviceType deviceType,
+            @NonNull List<String> groupMemberIds) {
+        mId = id;
+        mName = name;
+        mDescription = description;
+        mControlFilter = controlFilter;
+        mPlaybackStream = playbackStream;
+        mPlaybackType = playbackType;
+        mCanDisconnect = canDisconnect;
+        mVolumeHandling = volumeHandling;
+        mVolume = volume;
+        mVolumeMax = volumeMax;
+        mDeviceType = deviceType;
+        mGroupMemberIds = groupMemberIds;
+    }
+
+    /** Returns a deep copy of an existing {@link RouteItem}. */
+    @NonNull
+    public static RouteItem copyOf(@NonNull RouteItem routeItem) {
+        return new RouteItem(
+                routeItem.getId(),
+                routeItem.getName(),
+                routeItem.getDescription(),
+                routeItem.getControlFilter(),
+                routeItem.getPlaybackStream(),
+                routeItem.getPlaybackType(),
+                routeItem.isCanDisconnect(),
+                routeItem.getVolumeHandling(),
+                routeItem.getVolume(),
+                routeItem.getVolumeMax(),
+                routeItem.getDeviceType(),
+                routeItem.getGroupMemberIds());
+    }
+
     public enum ControlFilter {
         BASIC,
         QUEUE,
diff --git a/samples/MediaRoutingDemo/src/main/res/drawable/ic_add.xml b/samples/MediaRoutingDemo/src/main/res/drawable/ic_add.xml
new file mode 100644
index 0000000..9a27ae5
--- /dev/null
+++ b/samples/MediaRoutingDemo/src/main/res/drawable/ic_add.xml
@@ -0,0 +1,21 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- Copyright (C) 2023 The Android Open Source Project
+
+     Licensed under the Apache License, Version 2.0 (the "License");
+     you may not use this file except in compliance with the License.
+     You may obtain a copy of the License at
+
+          http://www.apache.org/licenses/LICENSE-2.0
+
+     Unless required by applicable law or agreed to in writing, software
+     distributed under the License is distributed on an "AS IS" BASIS,
+     WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+     See the License for the specific language governing permissions and
+     limitations under the License.
+-->
+<vector android:height="24dp" android:tint="#000000"
+    android:viewportHeight="24" android:viewportWidth="24"
+    android:width="24dp" xmlns:android="http://schemas.android.com/apk/res/android">
+    <path android:fillColor="@android:color/white" android:pathData="M19,13h-6v6h-2v-6H5v-2h6V5h2v6h6v2z"/>
+</vector>
+
diff --git a/samples/MediaRoutingDemo/src/main/res/drawable/ic_delete.xml b/samples/MediaRoutingDemo/src/main/res/drawable/ic_delete.xml
index b22b7fd..0bcc28b 100644
--- a/samples/MediaRoutingDemo/src/main/res/drawable/ic_delete.xml
+++ b/samples/MediaRoutingDemo/src/main/res/drawable/ic_delete.xml
@@ -13,7 +13,7 @@
      See the License for the specific language governing permissions and
      limitations under the License.
 -->
-<vector android:height="24dp" android:tint="#000000"
+<vector android:height="24dp" android:tint="#FFFFFF"
     android:viewportHeight="24" android:viewportWidth="24"
     android:width="24dp" xmlns:android="http://schemas.android.com/apk/res/android">
     <path android:fillColor="@android:color/white" android:pathData="M6,19c0,1.1 0.9,2 2,2h8c1.1,0 2,-0.9 2,-2V7H6v12zM19,4h-3.5l-1,-1h-5l-1,1H5v2h14V4z"/>
diff --git a/samples/MediaRoutingDemo/src/main/res/drawable/ic_edit.xml b/samples/MediaRoutingDemo/src/main/res/drawable/ic_edit.xml
index 89f261b..2d3ecfa 100644
--- a/samples/MediaRoutingDemo/src/main/res/drawable/ic_edit.xml
+++ b/samples/MediaRoutingDemo/src/main/res/drawable/ic_edit.xml
@@ -13,7 +13,7 @@
      See the License for the specific language governing permissions and
      limitations under the License.
 -->
-<vector android:height="24dp" android:tint="#000000"
+<vector android:height="24dp" android:tint="#FFFFFF"
     android:viewportHeight="24" android:viewportWidth="24"
     android:width="24dp" xmlns:android="http://schemas.android.com/apk/res/android">
     <path android:fillColor="@android:color/white" android:pathData="M3,17.25V21h3.75L17.81,9.94l-3.75,-3.75L3,17.25zM20.71,7.04c0.39,-0.39 0.39,-1.02 0,-1.41l-2.34,-2.34c-0.39,-0.39 -1.02,-0.39 -1.41,0l-1.83,1.83 3.75,3.75 1.83,-1.83z"/>
diff --git a/samples/MediaRoutingDemo/src/main/res/drawable/ic_settings.xml b/samples/MediaRoutingDemo/src/main/res/drawable/ic_settings.xml
new file mode 100644
index 0000000..4809650
--- /dev/null
+++ b/samples/MediaRoutingDemo/src/main/res/drawable/ic_settings.xml
@@ -0,0 +1,20 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- Copyright (C) 2023 The Android Open Source Project
+
+     Licensed under the Apache License, Version 2.0 (the "License");
+     you may not use this file except in compliance with the License.
+     You may obtain a copy of the License at
+
+          http://www.apache.org/licenses/LICENSE-2.0
+
+     Unless required by applicable law or agreed to in writing, software
+     distributed under the License is distributed on an "AS IS" BASIS,
+     WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+     See the License for the specific language governing permissions and
+     limitations under the License.
+-->
+<vector android:autoMirrored="true" android:height="24dp"
+    android:tint="#FFFFFF" android:viewportHeight="24"
+    android:viewportWidth="24" android:width="24dp" xmlns:android="http://schemas.android.com/apk/res/android">
+    <path android:fillColor="@android:color/white" android:pathData="M19.14,12.94c0.04,-0.3 0.06,-0.61 0.06,-0.94c0,-0.32 -0.02,-0.64 -0.07,-0.94l2.03,-1.58c0.18,-0.14 0.23,-0.41 0.12,-0.61l-1.92,-3.32c-0.12,-0.22 -0.37,-0.29 -0.59,-0.22l-2.39,0.96c-0.5,-0.38 -1.03,-0.7 -1.62,-0.94L14.4,2.81c-0.04,-0.24 -0.24,-0.41 -0.48,-0.41h-3.84c-0.24,0 -0.43,0.17 -0.47,0.41L9.25,5.35C8.66,5.59 8.12,5.92 7.63,6.29L5.24,5.33c-0.22,-0.08 -0.47,0 -0.59,0.22L2.74,8.87C2.62,9.08 2.66,9.34 2.86,9.48l2.03,1.58C4.84,11.36 4.8,11.69 4.8,12s0.02,0.64 0.07,0.94l-2.03,1.58c-0.18,0.14 -0.23,0.41 -0.12,0.61l1.92,3.32c0.12,0.22 0.37,0.29 0.59,0.22l2.39,-0.96c0.5,0.38 1.03,0.7 1.62,0.94l0.36,2.54c0.05,0.24 0.24,0.41 0.48,0.41h3.84c0.24,0 0.44,-0.17 0.47,-0.41l0.36,-2.54c0.59,-0.24 1.13,-0.56 1.62,-0.94l2.39,0.96c0.22,0.08 0.47,0 0.59,-0.22l1.92,-3.32c0.12,-0.22 0.07,-0.47 -0.12,-0.61L19.14,12.94zM12,15.6c-1.98,0 -3.6,-1.62 -3.6,-3.6s1.62,-3.6 3.6,-3.6s3.6,1.62 3.6,3.6S13.98,15.6 12,15.6z"/>
+</vector>
diff --git a/samples/MediaRoutingDemo/src/main/res/layout/activity_add_edit_route.xml b/samples/MediaRoutingDemo/src/main/res/layout/activity_add_edit_route.xml
new file mode 100644
index 0000000..8511ab91
--- /dev/null
+++ b/samples/MediaRoutingDemo/src/main/res/layout/activity_add_edit_route.xml
@@ -0,0 +1,295 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- Copyright (C) 2013 The Android Open Source Project
+
+     Licensed under the Apache License, Version 2.0 (the "License");
+     you may not use this file except in compliance with the License.
+     You may obtain a copy of the License at
+
+          http://www.apache.org/licenses/LICENSE-2.0
+
+     Unless required by applicable law or agreed to in writing, software
+     distributed under the License is distributed on an "AS IS" BASIS,
+     WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+     See the License for the specific language governing permissions and
+     limitations under the License.
+-->
+
+<!-- See corresponding Java code SampleMediaRouterActivity.java. -->
+
+<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
+    android:layout_width="match_parent"
+    android:layout_height="match_parent">
+
+    <ScrollView
+        android:layout_width="match_parent"
+        android:layout_height="match_parent">
+
+        <LinearLayout
+            android:layout_width="match_parent"
+            android:layout_height="wrap_content"
+            android:orientation="vertical">
+
+            <RelativeLayout
+                android:layout_width="match_parent"
+                android:layout_height="50dp"
+                android:layout_margin="12dp"
+                android:padding="4dp">
+
+                <TextView
+                    android:layout_width="wrap_content"
+                    android:layout_height="wrap_content"
+                    android:layout_alignParentLeft="true"
+                    android:layout_alignParentStart="true"
+                    android:layout_centerVertical="true"
+                    android:gravity="center"
+                    android:text="Name" />
+
+                <EditText
+                    android:id="@+id/name_edit_text"
+                    android:layout_width="150dp"
+                    android:layout_height="match_parent"
+                    android:layout_alignParentEnd="true"
+                    android:layout_alignParentRight="true"
+                    android:layout_centerVertical="true" />
+
+            </RelativeLayout>
+
+            <RelativeLayout
+                android:layout_width="match_parent"
+                android:layout_height="50dp"
+                android:layout_margin="12dp"
+                android:padding="4dp">
+
+                <TextView
+                    android:layout_width="wrap_content"
+                    android:layout_height="wrap_content"
+                    android:layout_alignParentLeft="true"
+                    android:layout_alignParentStart="true"
+                    android:layout_centerVertical="true"
+                    android:gravity="center"
+                    android:text="Description" />
+
+                <EditText
+                    android:id="@+id/description_edit_text"
+                    android:layout_width="150dp"
+                    android:layout_height="match_parent"
+                    android:layout_alignParentEnd="true"
+                    android:layout_alignParentRight="true"
+                    android:layout_centerVertical="true" />
+
+            </RelativeLayout>
+
+            <RelativeLayout
+                android:layout_width="match_parent"
+                android:layout_height="50dp"
+                android:layout_margin="12dp"
+                android:padding="4dp">
+
+                <TextView
+                    android:layout_width="wrap_content"
+                    android:layout_height="wrap_content"
+                    android:layout_alignParentLeft="true"
+                    android:layout_alignParentStart="true"
+                    android:layout_centerVertical="true"
+                    android:gravity="center"
+                    android:text="Control Filters" />
+
+                <Spinner
+                    android:id="@+id/control_filters_spinner"
+                    android:layout_width="180dp"
+                    android:layout_height="match_parent"
+                    android:layout_alignParentEnd="true"
+                    android:layout_alignParentRight="true"
+                    android:layout_centerVertical="true" />
+
+            </RelativeLayout>
+
+            <RelativeLayout
+                android:layout_width="match_parent"
+                android:layout_height="50dp"
+                android:layout_margin="12dp"
+                android:padding="4dp">
+
+                <TextView
+                    android:layout_width="wrap_content"
+                    android:layout_height="wrap_content"
+                    android:layout_alignParentLeft="true"
+                    android:layout_alignParentStart="true"
+                    android:layout_centerVertical="true"
+                    android:gravity="center"
+                    android:text="Playback Stream" />
+
+                <Spinner
+                    android:id="@+id/playback_stream_spinner"
+                    android:layout_width="180dp"
+                    android:layout_height="match_parent"
+                    android:layout_alignParentEnd="true"
+                    android:layout_alignParentRight="true"
+                    android:layout_centerVertical="true" />
+
+            </RelativeLayout>
+
+            <RelativeLayout
+                android:layout_width="match_parent"
+                android:layout_height="50dp"
+                android:layout_margin="12dp"
+                android:padding="4dp">
+
+                <TextView
+                    android:layout_width="wrap_content"
+                    android:layout_height="wrap_content"
+                    android:layout_alignParentLeft="true"
+                    android:layout_alignParentStart="true"
+                    android:layout_centerVertical="true"
+                    android:gravity="center"
+                    android:text="Playback Type" />
+
+                <Spinner
+                    android:id="@+id/playback_type_spinner"
+                    android:layout_width="180dp"
+                    android:layout_height="match_parent"
+                    android:layout_alignParentEnd="true"
+                    android:layout_alignParentRight="true"
+                    android:layout_centerVertical="true" />
+
+            </RelativeLayout>
+
+            <RelativeLayout
+                android:layout_width="match_parent"
+                android:layout_height="50dp"
+                android:layout_margin="12dp"
+                android:padding="4dp">
+
+                <TextView
+                    android:layout_width="wrap_content"
+                    android:layout_height="wrap_content"
+                    android:layout_alignParentLeft="true"
+                    android:layout_alignParentStart="true"
+                    android:layout_centerVertical="true"
+                    android:gravity="center"
+                    android:text="Device Type" />
+
+                <Spinner
+                    android:id="@+id/device_type_spinner"
+                    android:layout_width="180dp"
+                    android:layout_height="match_parent"
+                    android:layout_alignParentEnd="true"
+                    android:layout_alignParentRight="true"
+                    android:layout_centerVertical="true" />
+
+            </RelativeLayout>
+
+            <RelativeLayout
+                android:layout_width="match_parent"
+                android:layout_height="50dp"
+                android:layout_margin="12dp"
+                android:padding="4dp">
+
+                <TextView
+                    android:layout_width="wrap_content"
+                    android:layout_height="wrap_content"
+                    android:layout_alignParentLeft="true"
+                    android:layout_alignParentStart="true"
+                    android:layout_centerVertical="true"
+                    android:gravity="center"
+                    android:text="Volume Handling" />
+
+                <Spinner
+                    android:id="@+id/volume_handling_spinner"
+                    android:layout_width="180dp"
+                    android:layout_height="match_parent"
+                    android:layout_alignParentEnd="true"
+                    android:layout_alignParentRight="true"
+                    android:layout_centerVertical="true" />
+
+            </RelativeLayout>
+
+            <RelativeLayout
+                android:layout_width="match_parent"
+                android:layout_height="50dp"
+                android:layout_margin="12dp"
+                android:padding="4dp">
+
+                <TextView
+                    android:layout_width="wrap_content"
+                    android:layout_height="wrap_content"
+                    android:layout_alignParentLeft="true"
+                    android:layout_alignParentStart="true"
+                    android:layout_centerVertical="true"
+                    android:gravity="center"
+                    android:text="Volume" />
+
+                <EditText
+                    android:id="@+id/volume_edit_text"
+                    android:layout_width="150dp"
+                    android:layout_height="match_parent"
+                    android:inputType="number"
+                    android:layout_alignParentEnd="true"
+                    android:layout_alignParentRight="true"
+                    android:layout_centerVertical="true" />
+
+            </RelativeLayout>
+
+            <RelativeLayout
+                android:layout_width="match_parent"
+                android:layout_height="50dp"
+                android:layout_margin="12dp"
+                android:padding="4dp">
+
+                <TextView
+                    android:layout_width="wrap_content"
+                    android:layout_height="wrap_content"
+                    android:layout_alignParentLeft="true"
+                    android:layout_alignParentStart="true"
+                    android:layout_centerVertical="true"
+                    android:gravity="center"
+                    android:text="Volume Max" />
+
+                <EditText
+                    android:id="@+id/volume_max_edit_text"
+                    android:layout_width="150dp"
+                    android:layout_height="match_parent"
+                    android:inputType="number"
+                    android:layout_alignParentEnd="true"
+                    android:layout_alignParentRight="true"
+                    android:layout_centerVertical="true" />
+
+            </RelativeLayout>
+
+            <RelativeLayout
+                android:layout_width="match_parent"
+                android:layout_height="50dp"
+                android:layout_margin="12dp"
+                android:padding="4dp">
+
+                <Switch
+                    android:id="@+id/cam_disconnect_switch"
+                    android:layout_width="wrap_content"
+                    android:layout_height="match_parent"
+                    android:layout_alignParentEnd="true"
+                    android:layout_alignParentRight="true"
+                    android:layout_centerVertical="true" />
+
+                <TextView
+                    android:layout_width="wrap_content"
+                    android:layout_height="wrap_content"
+                    android:layout_alignParentLeft="true"
+                    android:layout_alignParentStart="true"
+                    android:layout_centerVertical="true"
+                    android:gravity="center"
+                    android:text="Can disconnect" />
+
+            </RelativeLayout>
+
+            <Button
+                android:id="@+id/save_button"
+                android:layout_height="wrap_content"
+                android:layout_width="match_parent"
+                android:layout_margin="16dp"
+                android:text="Save"/>
+
+        </LinearLayout>
+
+    </ScrollView>
+
+</LinearLayout>
\ No newline at end of file
diff --git a/samples/MediaRoutingDemo/src/main/res/layout/activity_settings.xml b/samples/MediaRoutingDemo/src/main/res/layout/activity_settings.xml
index 9fb6965..8432164 100644
--- a/samples/MediaRoutingDemo/src/main/res/layout/activity_settings.xml
+++ b/samples/MediaRoutingDemo/src/main/res/layout/activity_settings.xml
@@ -1,5 +1,4 @@
-<?xml version="1.0" encoding="utf-8"?>
-<!-- Copyright (C) 2013 The Android Open Source Project
+<?xml version="1.0" encoding="utf-8"?><!-- Copyright (C) 2013 The Android Open 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,100 +13,108 @@
      limitations under the License.
 -->
 
-<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
+<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
     android:layout_width="match_parent"
-    android:layout_height="match_parent">
+    android:layout_height="match_parent"
+    xmlns:app="http://schemas.android.com/apk/res-auto">
 
-    <ScrollView
+    <LinearLayout
         android:layout_width="match_parent"
-        android:layout_height="match_parent">
+        android:layout_height="wrap_content"
+        android:orientation="vertical">
 
-        <LinearLayout
+        <RelativeLayout
             android:layout_width="match_parent"
-            android:layout_height="wrap_content"
-            android:orientation="vertical">
+            android:layout_height="50dp"
+            android:layout_margin="12dp"
+            android:padding="4dp">
 
-            <RelativeLayout
-                android:layout_width="match_parent"
-                android:layout_height="50dp"
-                android:layout_margin="12dp"
-                android:padding="4dp">
+            <Switch
+                android:id="@+id/show_this_phone_switch"
+                android:layout_width="wrap_content"
+                android:layout_height="match_parent"
+                android:layout_alignParentEnd="true"
+                android:layout_alignParentRight="true"
+                android:layout_centerVertical="true" />
 
-                <Switch
-                    android:id="@+id/show_this_phone_switch"
-                    android:layout_width="wrap_content"
-                    android:layout_height="match_parent"
-                    android:layout_alignParentEnd="true"
-                    android:layout_alignParentRight="true"
-                    android:layout_centerVertical="true" />
+            <TextView
+                android:layout_width="wrap_content"
+                android:layout_height="wrap_content"
+                android:layout_alignParentLeft="true"
+                android:layout_alignParentStart="true"
+                android:layout_centerVertical="true"
+                android:gravity="center"
+                android:text="Transfer to local Enabled" />
 
-                <TextView
-                    android:layout_width="wrap_content"
-                    android:layout_height="wrap_content"
-                    android:layout_alignParentLeft="true"
-                    android:layout_alignParentStart="true"
-                    android:layout_centerVertical="true"
-                    android:gravity="center"
-                    android:text="Transfer to local Enabled" />
+        </RelativeLayout>
 
-            </RelativeLayout>
+        <RelativeLayout
+            android:layout_width="match_parent"
+            android:layout_height="50dp"
+            android:layout_margin="12dp"
+            android:padding="4dp">
 
-            <RelativeLayout
-                android:layout_width="match_parent"
-                android:layout_height="50dp"
-                android:layout_margin="12dp"
-                android:padding="4dp">
+            <Switch
+                android:id="@+id/dynamic_routing_switch"
+                android:layout_width="wrap_content"
+                android:layout_height="match_parent"
+                android:layout_alignParentEnd="true"
+                android:layout_alignParentRight="true"
+                android:layout_centerVertical="true" />
 
-                <Switch
-                    android:id="@+id/dynamic_routing_switch"
-                    android:layout_width="wrap_content"
-                    android:layout_height="match_parent"
-                    android:layout_alignParentEnd="true"
-                    android:layout_alignParentRight="true"
-                    android:layout_centerVertical="true" />
+            <TextView
+                android:layout_width="wrap_content"
+                android:layout_height="wrap_content"
+                android:layout_alignParentLeft="true"
+                android:layout_alignParentStart="true"
+                android:layout_centerVertical="true"
+                android:gravity="center"
+                android:text="Enable dynamic routing (For next routes only)" />
 
-                <TextView
-                    android:layout_width="wrap_content"
-                    android:layout_height="wrap_content"
-                    android:layout_alignParentLeft="true"
-                    android:layout_alignParentStart="true"
-                    android:layout_centerVertical="true"
-                    android:gravity="center"
-                    android:text="Enable dynamic routing (For next routes only)" />
+        </RelativeLayout>
 
-            </RelativeLayout>
+        <RelativeLayout
+            android:layout_width="match_parent"
+            android:layout_height="50dp"
+            android:layout_margin="12dp"
+            android:padding="4dp">
 
-            <RelativeLayout
-                android:layout_width="match_parent"
-                android:layout_height="50dp"
-                android:layout_margin="12dp"
-                android:padding="4dp">
+            <Spinner
+                android:id="@+id/dialog_spinner"
+                android:layout_width="wrap_content"
+                android:layout_height="match_parent"
+                android:layout_alignParentEnd="true"
+                android:layout_alignParentRight="true"
+                android:layout_centerVertical="true" />
 
-                <Spinner
-                    android:id="@+id/dialog_spinner"
-                    android:layout_width="wrap_content"
-                    android:layout_height="match_parent"
-                    android:layout_alignParentEnd="true"
-                    android:layout_alignParentRight="true"
-                    android:layout_centerVertical="true" />
+            <TextView
+                android:layout_width="wrap_content"
+                android:layout_height="wrap_content"
+                android:layout_alignParentLeft="true"
+                android:layout_alignParentStart="true"
+                android:layout_centerVertical="true"
+                android:gravity="center"
+                android:text="Dialog Type" />
 
-                <TextView
-                    android:layout_width="wrap_content"
-                    android:layout_height="wrap_content"
-                    android:layout_alignParentLeft="true"
-                    android:layout_alignParentStart="true"
-                    android:layout_centerVertical="true"
-                    android:gravity="center"
-                    android:text="Dialog Type" />
+        </RelativeLayout>
 
-            </RelativeLayout>
+        <androidx.recyclerview.widget.RecyclerView
+            android:id="@+id/routes_recycler_view"
+            android:layout_width="match_parent"
+            android:layout_height="0dp"
+            android:layout_weight="1" />
 
-            <androidx.recyclerview.widget.RecyclerView
-                android:id="@+id/routes_recycler_view"
-                android:layout_width="match_parent"
-                android:layout_height="wrap_content" />
+    </LinearLayout>
 
-        </LinearLayout>
+    <com.google.android.material.floatingactionbutton.FloatingActionButton
+        android:id="@+id/new_route_button"
+        android:layout_width="wrap_content"
+        android:layout_height="wrap_content"
+        android:layout_alignParentBottom="true"
+        android:layout_alignParentEnd="true"
+        android:layout_alignParentRight="true"
+        android:layout_margin="20dp"
+        android:padding="0dp"
+        app:srcCompat="@drawable/ic_add" />
 
-    </ScrollView>
-</LinearLayout>
\ No newline at end of file
+</RelativeLayout>
\ No newline at end of file
diff --git a/samples/MediaRoutingDemo/src/main/res/layout/route_item.xml b/samples/MediaRoutingDemo/src/main/res/layout/route_item.xml
index 4e1c191..4a7fe52 100644
--- a/samples/MediaRoutingDemo/src/main/res/layout/route_item.xml
+++ b/samples/MediaRoutingDemo/src/main/res/layout/route_item.xml
@@ -16,13 +16,16 @@
 <!-- Layout for list item in routes list view. Displays ImageButtons
      instead to the right of the item for edit and delete. -->
 
-<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
+<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
     xmlns:app="http://schemas.android.com/apk/res-auto"
+    android:layout_margin="16dp"
+    android:orientation="horizontal"
     android:layout_width="match_parent"
-    android:layout_height="match_parent">
+    android:layout_height="wrap_content">
 
     <LinearLayout
-        android:layout_width="wrap_content"
+        android:layout_width="0dp"
+        android:layout_weight="1"
         android:layout_height="match_parent"
         android:layout_alignParentLeft="true"
         android:layout_alignParentStart="true"
@@ -33,18 +36,18 @@
             android:layout_width="wrap_content"
             android:layout_height="wrap_content"
             android:layout_gravity="left"
-            android:layout_margin="8dp"
+            android:layout_marginVertical="2dp"
             android:gravity="left"
-            android:textAppearance="?android:attr/textAppearanceLarge" />
+            android:textSize="18sp" />
 
         <TextView
             android:id="@+id/description_textview"
             android:layout_width="wrap_content"
             android:layout_height="wrap_content"
             android:layout_gravity="left"
-            android:layout_margin="8dp"
+            android:layout_marginVertical="2dp"
             android:gravity="left"
-            android:textAppearance="?android:attr/textAppearanceLarge" />
+            android:textSize="14sp" />
 
     </LinearLayout>
 
@@ -61,6 +64,7 @@
             android:layout_width="wrap_content"
             android:layout_height="wrap_content"
             android:layout_margin="8dp"
+            android:background="@android:color/transparent"
             app:srcCompat="@drawable/ic_edit" />
 
         <ImageButton
@@ -68,8 +72,9 @@
             android:layout_width="wrap_content"
             android:layout_height="wrap_content"
             android:layout_margin="8dp"
+            android:background="@android:color/transparent"
             app:srcCompat="@drawable/ic_delete" />
 
     </LinearLayout>
 
-</RelativeLayout>
+</LinearLayout>
diff --git a/samples/MediaRoutingDemo/src/main/res/menu/sample_media_router_menu.xml b/samples/MediaRoutingDemo/src/main/res/menu/sample_media_router_menu.xml
index aef85a3..be15191 100644
--- a/samples/MediaRoutingDemo/src/main/res/menu/sample_media_router_menu.xml
+++ b/samples/MediaRoutingDemo/src/main/res/menu/sample_media_router_menu.xml
@@ -23,5 +23,6 @@
 
     <item android:id="@+id/settings_menu_item"
         android:title="@string/settings_menu_item"
-        app:showAsAction="withText" />
+        android:icon="@drawable/ic_settings"
+        app:showAsAction="always" />
 </menu>
diff --git a/samples/Support4Demos/src/main/java/com/example/android/supportv4/view/WindowInsetsControllerPlayground.kt b/samples/Support4Demos/src/main/java/com/example/android/supportv4/view/WindowInsetsControllerPlayground.kt
index 63cf794..67fee43 100644
--- a/samples/Support4Demos/src/main/java/com/example/android/supportv4/view/WindowInsetsControllerPlayground.kt
+++ b/samples/Support4Demos/src/main/java/com/example/android/supportv4/view/WindowInsetsControllerPlayground.kt
@@ -487,9 +487,10 @@
 
     private fun setupBehaviorSpinner() {
         val types = mapOf(
-            "BY TOUCH" to WindowInsetsControllerCompat.BEHAVIOR_SHOW_BARS_BY_TOUCH,
-            "BY SWIPE" to WindowInsetsControllerCompat.BEHAVIOR_SHOW_BARS_BY_SWIPE,
+            "DEFAULT" to WindowInsetsControllerCompat.BEHAVIOR_DEFAULT,
             "TRANSIENT" to WindowInsetsControllerCompat.BEHAVIOR_SHOW_TRANSIENT_BARS_BY_SWIPE,
+            "BY TOUCH (Deprecated)" to WindowInsetsControllerCompat.BEHAVIOR_SHOW_BARS_BY_TOUCH,
+            "BY SWIPE (Deprecated)" to WindowInsetsControllerCompat.BEHAVIOR_SHOW_BARS_BY_SWIPE,
         )
         findViewById<Spinner>(R.id.spn_behavior).apply {
             adapter = ArrayAdapter(
diff --git a/settings.gradle b/settings.gradle
index 45819d9..a098059 100644
--- a/settings.gradle
+++ b/settings.gradle
@@ -194,7 +194,8 @@
     TOOLS,
     KMP,
     CAMERA,
-    NATIVE
+    NATIVE,
+    WINDOW,
 }
 
 private String getRequestedProjectSubsetName() {
@@ -255,6 +256,9 @@
             case "NATIVE":
                 filter.add(BuildType.NATIVE)
                 break
+            case "WINDOW":
+                filter.add(BuildType.WINDOW)
+                break
             case "ALL":
                 // Return null so that no filtering is done
                 return null
@@ -270,6 +274,7 @@
                         "MEDIA   - media, media2, and mediarouter projects\n" +
                         "WEAR    - Wear OS projects\n" +
                         "NATIVE  - native projects\n" +
+                        "WINDOW  - window projects\n" +
                         "GLANCE  - glance projects")
         }
     }
@@ -361,6 +366,7 @@
 includeProject(":annotation:annotation-experimental-lint-integration-tests", "annotation/annotation-experimental-lint/integration-tests")
 includeProject(":annotation:annotation-sampled")
 includeProject(":appactions:interaction:interaction-proto", [BuildType.MAIN])
+includeProject(":appactions:interaction:interaction-capabilities-core", [BuildType.MAIN])
 includeProject(":appcompat:appcompat", [BuildType.MAIN])
 includeProject(":appcompat:appcompat-benchmark", [BuildType.MAIN])
 includeProject(":appcompat:appcompat-lint", [BuildType.MAIN])
@@ -795,6 +801,8 @@
 includeProject(":preference:preference", [BuildType.MAIN])
 includeProject(":preference:preference-ktx", [BuildType.MAIN])
 includeProject(":print:print", [BuildType.MAIN])
+includeProject(":privacysandbox:ads:ads-adservices", [BuildType.MAIN])
+includeProject(":privacysandbox:ads:ads-adservices-java", [BuildType.MAIN])
 includeProject(":privacysandbox:sdkruntime:sdkruntime-client", [BuildType.MAIN])
 includeProject(":privacysandbox:sdkruntime:sdkruntime-core", [BuildType.MAIN])
 includeProject(":privacysandbox:tools:tools", [BuildType.MAIN])
@@ -965,17 +973,17 @@
 includeProject(":wear:watchface:watchface-style-old-api-test-stub", "wear/watchface/watchface-style/old-api-test-stub", [BuildType.MAIN, BuildType.WEAR])
 includeProject(":webkit:integration-tests:testapp", [BuildType.MAIN])
 includeProject(":webkit:webkit", [BuildType.MAIN])
-includeProject(":window:window", [BuildType.MAIN, BuildType.COMPOSE, BuildType.FLAN, BuildType.CAMERA])
-includeProject(":window:extensions:extensions", [BuildType.MAIN, BuildType.COMPOSE, BuildType.FLAN, BuildType.CAMERA])
-includeProject(":window:extensions:core:core", [BuildType.MAIN, BuildType.COMPOSE, BuildType.FLAN, BuildType.CAMERA])
-includeProject(":window:integration-tests:configuration-change-tests", [BuildType.MAIN])
-includeProject(":window:sidecar:sidecar", [BuildType.MAIN, BuildType.COMPOSE, BuildType.FLAN, BuildType.CAMERA])
-includeProject(":window:window-java", [BuildType.MAIN, BuildType.COMPOSE, BuildType.FLAN, BuildType.CAMERA])
-includeProject(":window:window-core", [BuildType.MAIN, BuildType.COMPOSE, BuildType.FLAN, BuildType.CAMERA])
-includeProject(":window:window-rxjava2", [BuildType.MAIN])
-includeProject(":window:window-rxjava3", [BuildType.MAIN])
-includeProject(":window:window-samples", [BuildType.MAIN, BuildType.COMPOSE, BuildType.FLAN, BuildType.CAMERA])
-includeProject(":window:window-testing", [BuildType.MAIN, BuildType.COMPOSE, BuildType.FLAN, BuildType.CAMERA])
+includeProject(":window:window", [BuildType.MAIN, BuildType.COMPOSE, BuildType.FLAN, BuildType.CAMERA, BuildType.WINDOW])
+includeProject(":window:extensions:extensions", [BuildType.MAIN, BuildType.COMPOSE, BuildType.FLAN, BuildType.CAMERA, BuildType.WINDOW])
+includeProject(":window:extensions:core:core", [BuildType.MAIN, BuildType.COMPOSE, BuildType.FLAN, BuildType.CAMERA, BuildType.WINDOW])
+includeProject(":window:integration-tests:configuration-change-tests", [BuildType.MAIN, BuildType.WINDOW])
+includeProject(":window:sidecar:sidecar", [BuildType.MAIN, BuildType.COMPOSE, BuildType.FLAN, BuildType.CAMERA, BuildType.WINDOW])
+includeProject(":window:window-java", [BuildType.MAIN, BuildType.COMPOSE, BuildType.FLAN, BuildType.CAMERA, BuildType.WINDOW])
+includeProject(":window:window-core", [BuildType.MAIN, BuildType.COMPOSE, BuildType.FLAN, BuildType.CAMERA, BuildType.WINDOW])
+includeProject(":window:window-rxjava2", [BuildType.MAIN, BuildType.WINDOW])
+includeProject(":window:window-rxjava3", [BuildType.MAIN, BuildType.WINDOW])
+includeProject(":window:window-samples", [BuildType.MAIN, BuildType.COMPOSE, BuildType.FLAN, BuildType.CAMERA, BuildType.WINDOW])
+includeProject(":window:window-testing", [BuildType.MAIN, BuildType.COMPOSE, BuildType.FLAN, BuildType.CAMERA, BuildType.WINDOW])
 includeProject(":work:integration-tests:testapp", [BuildType.MAIN])
 includeProject(":work:work-benchmark", [BuildType.MAIN])
 includeProject(":work:work-gcm", [BuildType.MAIN])
diff --git a/studiow b/studiow
index 213da89..f76fba3 100755
--- a/studiow
+++ b/studiow
@@ -120,6 +120,9 @@
   if [ "$subsetArg" == "t" -o "$subsetArg" == "tools" ]; then
     newSubset=tools
   fi
+  if [ "$subsetArg" == "wm" -o "$subsetArg" == "window" ]; then
+    newSubset=window
+  fi
   if [ "$newSubset" == "" ]; then
     echo "Unrecognized argument: '$subsetArg'"
     usage
diff --git a/test/uiautomator/integration-tests/testapp/src/androidTest/java/androidx/test/uiautomator/testapp/BaseTest.java b/test/uiautomator/integration-tests/testapp/src/androidTest/java/androidx/test/uiautomator/testapp/BaseTest.java
index 95c305a..ca89e89 100644
--- a/test/uiautomator/integration-tests/testapp/src/androidTest/java/androidx/test/uiautomator/testapp/BaseTest.java
+++ b/test/uiautomator/integration-tests/testapp/src/androidTest/java/androidx/test/uiautomator/testapp/BaseTest.java
@@ -56,13 +56,8 @@
     }
 
     protected void launchTestActivity(@NonNull Class<? extends Activity> activity) {
-        launchTestActivity(activity, new Intent().setFlags(DEFAULT_FLAGS));
-    }
-
-    protected void launchTestActivity(@NonNull Class<? extends Activity> activity,
-            @NonNull Intent intent) {
         Context context = ApplicationProvider.getApplicationContext();
-        context.startActivity(new Intent(intent).setClass(context, activity));
+        context.startActivity(new Intent().setFlags(DEFAULT_FLAGS).setClass(context, activity));
         assertTrue("Test app not visible after launching activity",
                 mDevice.wait(Until.hasObject(By.pkg(TEST_APP)), TIMEOUT_MS));
     }
diff --git a/test/uiautomator/integration-tests/testapp/src/androidTest/java/androidx/test/uiautomator/testapp/MultiWindowTest.java b/test/uiautomator/integration-tests/testapp/src/androidTest/java/androidx/test/uiautomator/testapp/MultiWindowTest.java
index 016feb1..2974a4c 100644
--- a/test/uiautomator/integration-tests/testapp/src/androidTest/java/androidx/test/uiautomator/testapp/MultiWindowTest.java
+++ b/test/uiautomator/integration-tests/testapp/src/androidTest/java/androidx/test/uiautomator/testapp/MultiWindowTest.java
@@ -16,24 +16,15 @@
 
 package androidx.test.uiautomator.testapp;
 
-import static android.accessibilityservice.AccessibilityService.GLOBAL_ACTION_TOGGLE_SPLIT_SCREEN;
-import static android.content.Intent.FLAG_ACTIVITY_LAUNCH_ADJACENT;
-import static android.content.Intent.FLAG_ACTIVITY_MULTIPLE_TASK;
-import static android.content.Intent.FLAG_ACTIVITY_NEW_TASK;
-
-import static androidx.test.uiautomator.testapp.SplitScreenTestActivity.WINDOW_ID;
-
 import static org.junit.Assert.assertEquals;
 import static org.junit.Assert.assertNotNull;
 import static org.junit.Assert.assertTrue;
 
-import android.content.Intent;
 import android.graphics.Rect;
 import android.os.SystemClock;
 
 import androidx.test.filters.LargeTest;
 import androidx.test.filters.SdkSuppress;
-import androidx.test.platform.app.InstrumentationRegistry;
 import androidx.test.uiautomator.By;
 import androidx.test.uiautomator.BySelector;
 import androidx.test.uiautomator.Configurator;
@@ -43,9 +34,6 @@
 import org.junit.Ignore;
 import org.junit.Test;
 
-import java.util.Arrays;
-import java.util.List;
-
 /** Integration tests for multi-window support. */
 @LargeTest
 public class MultiWindowTest extends BaseTest {
@@ -99,16 +87,10 @@
     }
 
     @Test
-    @SdkSuppress(minSdkVersion = 31, maxSdkVersion = 33) // b/262909049: Failing on SDK 34
+    @SdkSuppress(minSdkVersion = 32)
     public void testMultiWindow_splitScreen() {
         // Launch two split-screen activities with different IDs.
-        launchTestActivity(SplitScreenTestActivity.class,
-                new Intent().setFlags(DEFAULT_FLAGS).putExtra(WINDOW_ID, "first"));
-        InstrumentationRegistry.getInstrumentation().getUiAutomation()
-                .performGlobalAction(GLOBAL_ACTION_TOGGLE_SPLIT_SCREEN);
-        launchTestActivity(SplitScreenTestActivity.class,
-                new Intent().setFlags(FLAG_ACTIVITY_NEW_TASK | FLAG_ACTIVITY_LAUNCH_ADJACENT
-                        | FLAG_ACTIVITY_MULTIPLE_TASK).putExtra(WINDOW_ID, "second"));
+        launchTestActivity(SplitScreenTestActivity.class);
         SystemClock.sleep(TRANSITION_DELAY_MS); // Wait for the windows to settle.
 
         // Both split screen windows are present and searchable.
@@ -117,38 +99,12 @@
         UiObject2 secondWindow = mDevice.findObject(By.res(TEST_APP, "window_id").text("second"));
         assertNotNull(secondWindow);
 
-        // Window IDs are centered in each window (bounds correctly calculated; order independent).
-        int width = mDevice.getDisplayWidth();
-        int height = mDevice.getDisplayHeight();
-        List<UiObject2> windows = Arrays.asList(firstWindow, secondWindow);
-        assertTrue(windows.stream().anyMatch(
-                w -> w.getVisibleBounds().contains(width / 2, height / 4)));
-        assertTrue(windows.stream().anyMatch(
-                w -> w.getVisibleBounds().contains(width / 2, 3 * height / 4)));
-    }
-
-    @Test
-    @SdkSuppress(minSdkVersion = 31, maxSdkVersion = 33) // b/262909049: Failing on SDK 34
-    public void testMultiWindow_click() {
-        // Launch two split-screen activities with buttons.
-        launchTestActivity(UiDeviceTestClickActivity.class);
-        InstrumentationRegistry.getInstrumentation().getUiAutomation()
-                .performGlobalAction(GLOBAL_ACTION_TOGGLE_SPLIT_SCREEN);
-        launchTestActivity(UiDeviceTestClickActivity.class,
-                new Intent().setFlags(FLAG_ACTIVITY_NEW_TASK | FLAG_ACTIVITY_LAUNCH_ADJACENT
-                        | FLAG_ACTIVITY_MULTIPLE_TASK));
-        SystemClock.sleep(TRANSITION_DELAY_MS); // Wait for the windows to settle.
-
-        // Click a button in the middle of each activity.
+        // Operations (clicks) and coordinates are valid in both split screen windows.
         int width = mDevice.getDisplayWidth();
         int height = mDevice.getDisplayHeight();
         mDevice.click(width / 2, height / 4);
         mDevice.click(width / 2, 3 * height / 4);
-
-        // Verify that both buttons were clicked.
-        List<UiObject2> buttons = mDevice.findObjects(By.res(TEST_APP, "button"));
-        assertEquals(2, buttons.size());
-        assertEquals("I've been clicked!", buttons.get(0).getText());
-        assertEquals("I've been clicked!", buttons.get(1).getText());
+        assertEquals("I've been clicked!", firstWindow.getText());
+        assertEquals("I've been clicked!", secondWindow.getText());
     }
 }
diff --git a/test/uiautomator/integration-tests/testapp/src/androidTest/java/androidx/test/uiautomator/testapp/UiDeviceTest.java b/test/uiautomator/integration-tests/testapp/src/androidTest/java/androidx/test/uiautomator/testapp/UiDeviceTest.java
index c23d695..f314dcd 100644
--- a/test/uiautomator/integration-tests/testapp/src/androidTest/java/androidx/test/uiautomator/testapp/UiDeviceTest.java
+++ b/test/uiautomator/integration-tests/testapp/src/androidTest/java/androidx/test/uiautomator/testapp/UiDeviceTest.java
@@ -246,22 +246,12 @@
 
     @Test
     public void testPressRecentApps() throws Exception {
-        launchTestActivity(KeycodeTestActivity.class);
+        launchTestActivity(MainActivity.class);
 
-        // No app name when the app is running.
-        assertFalse(mDevice.wait(Until.hasObject(By.text(APP_NAME)), TIMEOUT_MS));
-
+        // Test app appears in the "Recent Apps" screen after pressing button.
+        assertFalse(mDevice.wait(Until.hasObject(By.desc(APP_NAME)), TIMEOUT_MS));
         mDevice.pressRecentApps();
-
-        Pattern iconResIdPattern = Pattern.compile(".*launcher.*icon");
-        // For API 28 and above, click on the app icon to make the name visible.
-        if (mDevice.wait(Until.hasObject(By.res(iconResIdPattern)), TIMEOUT_MS)) {
-            UiObject2 icon = mDevice.findObject(By.res(iconResIdPattern));
-            icon.click();
-        }
-
-        // App name appears when on Recent screen.
-        assertTrue(mDevice.wait(Until.hasObject(By.text(APP_NAME)), TIMEOUT_MS));
+        assertTrue(mDevice.wait(Until.hasObject(By.desc(APP_NAME)), TIMEOUT_MS));
     }
 
     @Test
diff --git a/test/uiautomator/integration-tests/testapp/src/main/AndroidManifest.xml b/test/uiautomator/integration-tests/testapp/src/main/AndroidManifest.xml
index e5d08a1..e98a704 100644
--- a/test/uiautomator/integration-tests/testapp/src/main/AndroidManifest.xml
+++ b/test/uiautomator/integration-tests/testapp/src/main/AndroidManifest.xml
@@ -188,7 +188,6 @@
         </activity>
         <activity android:name=".UiDeviceTestClickActivity"
             android:exported="true"
-            android:configChanges="screenSize|smallestScreenSize|screenLayout|orientation"
             android:theme="@android:style/Theme.Holo.NoActionBar">
             <intent-filter>
                 <action android:name="android.intent.action.MAIN" />
diff --git a/test/uiautomator/integration-tests/testapp/src/main/java/androidx/test/uiautomator/testapp/SplitScreenTestActivity.java b/test/uiautomator/integration-tests/testapp/src/main/java/androidx/test/uiautomator/testapp/SplitScreenTestActivity.java
index d992405..963d70b 100644
--- a/test/uiautomator/integration-tests/testapp/src/main/java/androidx/test/uiautomator/testapp/SplitScreenTestActivity.java
+++ b/test/uiautomator/integration-tests/testapp/src/main/java/androidx/test/uiautomator/testapp/SplitScreenTestActivity.java
@@ -17,12 +17,15 @@
 package androidx.test.uiautomator.testapp;
 
 import android.app.Activity;
+import android.content.Intent;
 import android.os.Bundle;
 import android.widget.TextView;
 
 import androidx.annotation.Nullable;
+import androidx.annotation.RequiresApi;
 
 /** {@link Activity} for testing multi-window (split screen) functionality. */
+@RequiresApi(32) // FLAG_ACTIVITY_LAUNCH_ADJACENT may not work below API 32.
 public class SplitScreenTestActivity extends Activity {
 
     static final String WINDOW_ID = "WINDOW_ID";
@@ -31,7 +34,21 @@
     public void onCreate(@Nullable Bundle savedInstanceState) {
         super.onCreate(savedInstanceState);
         setContentView(R.layout.split_screen_test_activity);
+
+        // If this is the base activity, launch a secondary activity in split-screen.
         String windowId = getIntent().getStringExtra(WINDOW_ID);
-        ((TextView) findViewById(R.id.window_id)).setText(windowId);
+        if (windowId == null) {
+            windowId = "first";
+            startActivity(
+                    new Intent(this, SplitScreenTestActivity.class)
+                            .addFlags(Intent.FLAG_ACTIVITY_LAUNCH_ADJACENT
+                                    | Intent.FLAG_ACTIVITY_NEW_TASK
+                                    | Intent.FLAG_ACTIVITY_MULTIPLE_TASK)
+                            .putExtra(WINDOW_ID, "second"));
+        }
+
+        TextView text = findViewById(R.id.window_id);
+        text.setText(windowId);
+        text.setOnClickListener(v -> text.setText("I've been clicked!"));
     }
 }
diff --git a/tv/tv-foundation/src/androidTest/java/androidx/tv/foundation/lazy/grid/LazyScrollTest.kt b/tv/tv-foundation/src/androidTest/java/androidx/tv/foundation/lazy/grid/LazyScrollTest.kt
index b14edf9..fc4e60f 100644
--- a/tv/tv-foundation/src/androidTest/java/androidx/tv/foundation/lazy/grid/LazyScrollTest.kt
+++ b/tv/tv-foundation/src/androidTest/java/androidx/tv/foundation/lazy/grid/LazyScrollTest.kt
@@ -244,6 +244,25 @@
     }
 
     @Test
+    fun animateScrollToItemWithOffsetLargerThanItemSize_forward() = runBlocking {
+        withContext(Dispatchers.Main + AutoTestFrameClock()) {
+            state.animateScrollToItem(10, -itemSizePx * 3)
+        }
+        assertThat(state.firstVisibleItemIndex).isEqualTo(4)
+        assertThat(state.firstVisibleItemScrollOffset).isEqualTo(0)
+    }
+
+    @Test
+    fun animateScrollToItemWithOffsetLargerThanItemSize_backward() = runBlocking {
+        withContext(Dispatchers.Main + AutoTestFrameClock()) {
+            state.scrollToItem(10)
+            state.animateScrollToItem(0, itemSizePx * 3)
+        }
+        assertThat(state.firstVisibleItemIndex).isEqualTo(6)
+        assertThat(state.firstVisibleItemScrollOffset).isEqualTo(0)
+    }
+
+    @Test
     fun canScrollForward() = runBlocking {
         assertThat(state.firstVisibleItemScrollOffset).isEqualTo(0)
         assertThat(state.canScrollForward).isTrue()
diff --git a/tv/tv-foundation/src/androidTest/java/androidx/tv/foundation/lazy/list/LazyListPinnableContainerTest.kt b/tv/tv-foundation/src/androidTest/java/androidx/tv/foundation/lazy/list/LazyListPinnableContainerTest.kt
index 48ec709..98ba1e9 100644
--- a/tv/tv-foundation/src/androidTest/java/androidx/tv/foundation/lazy/list/LazyListPinnableContainerTest.kt
+++ b/tv/tv-foundation/src/androidTest/java/androidx/tv/foundation/lazy/list/LazyListPinnableContainerTest.kt
@@ -238,7 +238,7 @@
         }
 
         rule.runOnIdle {
-            handle.unpin()
+            handle.release()
         }
 
         rule.waitUntil {
@@ -529,7 +529,7 @@
         while (handles.isNotEmpty()) {
             rule.runOnIdle {
                 assertThat(composed).contains(1)
-                handles.removeFirst().unpin()
+                handles.removeFirst().release()
             }
         }
 
@@ -566,7 +566,7 @@
 
         rule.runOnIdle {
             assertThat(parentPinned).isTrue()
-            handle.unpin()
+            handle.release()
         }
 
         rule.runOnIdle {
diff --git a/tv/tv-foundation/src/androidTest/java/androidx/tv/foundation/lazy/list/LazyListTest.kt b/tv/tv-foundation/src/androidTest/java/androidx/tv/foundation/lazy/list/LazyListTest.kt
index 25fdd03..876b8eb 100644
--- a/tv/tv-foundation/src/androidTest/java/androidx/tv/foundation/lazy/list/LazyListTest.kt
+++ b/tv/tv-foundation/src/androidTest/java/androidx/tv/foundation/lazy/list/LazyListTest.kt
@@ -84,6 +84,7 @@
 import java.util.concurrent.CountDownLatch
 import kotlin.math.roundToInt
 import kotlinx.coroutines.runBlocking
+import org.junit.Ignore
 import org.junit.Test
 import org.junit.runner.RunWith
 import org.junit.runners.Parameterized
@@ -646,6 +647,7 @@
             .assertStartPositionIsAlmost(0.dp)
     }
 
+    @Ignore("b/266124027")
     @Test
     fun whenItemsBecameEmpty() {
         var items by mutableStateOf((1..10).toList())
diff --git a/tv/tv-foundation/src/androidTest/java/androidx/tv/foundation/lazy/list/LazyScrollTest.kt b/tv/tv-foundation/src/androidTest/java/androidx/tv/foundation/lazy/list/LazyScrollTest.kt
index e533a1e..638985f 100644
--- a/tv/tv-foundation/src/androidTest/java/androidx/tv/foundation/lazy/list/LazyScrollTest.kt
+++ b/tv/tv-foundation/src/androidTest/java/androidx/tv/foundation/lazy/list/LazyScrollTest.kt
@@ -240,6 +240,25 @@
     }
 
     @Test
+    fun animateScrollToItemWithOffsetLargerThanItemSize_forward() = runBlocking {
+        withContext(Dispatchers.Main + AutoTestFrameClock()) {
+            state.animateScrollToItem(10, -itemSizePx * 3)
+        }
+        assertThat(state.firstVisibleItemIndex).isEqualTo(7)
+        assertThat(state.firstVisibleItemScrollOffset).isEqualTo(0)
+    }
+
+    @Test
+    fun animateScrollToItemWithOffsetLargerThanItemSize_backward() = runBlocking {
+        withContext(Dispatchers.Main + AutoTestFrameClock()) {
+            state.scrollToItem(10)
+            state.animateScrollToItem(0, itemSizePx * 3)
+        }
+        assertThat(state.firstVisibleItemIndex).isEqualTo(3)
+        assertThat(state.firstVisibleItemScrollOffset).isEqualTo(0)
+    }
+
+    @Test
     fun canScrollForward() = runBlocking {
         assertThat(state.firstVisibleItemScrollOffset).isEqualTo(0)
         assertThat(state.canScrollForward).isTrue()
diff --git a/tv/tv-foundation/src/main/java/androidx/tv/foundation/lazy/grid/LazyGridAnimateScrollScope.kt b/tv/tv-foundation/src/main/java/androidx/tv/foundation/lazy/grid/LazyGridAnimateScrollScope.kt
index 6584f8f..a96a483 100644
--- a/tv/tv-foundation/src/main/java/androidx/tv/foundation/lazy/grid/LazyGridAnimateScrollScope.kt
+++ b/tv/tv-foundation/src/main/java/androidx/tv/foundation/lazy/grid/LazyGridAnimateScrollScope.kt
@@ -20,6 +20,7 @@
 import androidx.compose.ui.unit.Density
 import androidx.compose.ui.util.fastFirstOrNull
 import androidx.tv.foundation.lazy.layout.LazyAnimateScrollScope
+import kotlin.math.abs
 import kotlin.math.max
 
 internal class LazyGridAnimateScrollScope(
@@ -64,8 +65,10 @@
             (index - firstVisibleItemIndex + (slotsPerLine - 1) * if (before) -1 else 1) /
                 slotsPerLine
 
+        var coercedOffset = minOf(abs(targetScrollOffset), averageLineMainAxisSize)
+        if (targetScrollOffset < 0) coercedOffset *= -1
         return (averageLineMainAxisSize * linesDiff).toFloat() +
-            targetScrollOffset - firstVisibleItemScrollOffset
+            coercedOffset - firstVisibleItemScrollOffset
     }
 
     override val numOfItemsForTeleport: Int get() = 100 * state.slotsPerLine
diff --git a/tv/tv-foundation/src/main/java/androidx/tv/foundation/lazy/layout/LazyAnimateScroll.kt b/tv/tv-foundation/src/main/java/androidx/tv/foundation/lazy/layout/LazyAnimateScroll.kt
index 06ecb8d..c6ec353 100644
--- a/tv/tv-foundation/src/main/java/androidx/tv/foundation/lazy/layout/LazyAnimateScroll.kt
+++ b/tv/tv-foundation/src/main/java/androidx/tv/foundation/lazy/layout/LazyAnimateScroll.kt
@@ -33,6 +33,7 @@
 
 private val TargetDistance = 2500.dp
 private val BoundDistance = 1500.dp
+private val MinimumDistance = 50.dp
 
 private const val DEBUG = false
 private inline fun debugLog(generateMsg: () -> String) {
@@ -77,6 +78,7 @@
         try {
             val targetDistancePx = with(density) { TargetDistance.toPx() }
             val boundDistancePx = with(density) { BoundDistance.toPx() }
+            val minDistancePx = with(density) { MinimumDistance.toPx() }
             var loop = true
             var anim = AnimationState(0f)
             val targetItemInitialOffset = getTargetItemOffset(index)
@@ -118,7 +120,8 @@
             while (loop && itemCount > 0) {
                 val expectedDistance = expectedDistanceTo(index, scrollOffset)
                 val target = if (abs(expectedDistance) < targetDistancePx) {
-                    expectedDistance
+                    val absTargetPx = maxOf(kotlin.math.abs(expectedDistance), minDistancePx)
+                    if (forward) absTargetPx else -absTargetPx
                 } else {
                     if (forward) targetDistancePx else -targetDistancePx
                 }
diff --git a/tv/tv-foundation/src/main/java/androidx/tv/foundation/lazy/list/LazyListAnimateScrollScope.kt b/tv/tv-foundation/src/main/java/androidx/tv/foundation/lazy/list/LazyListAnimateScrollScope.kt
index 510480d..1bdb3c2 100644
--- a/tv/tv-foundation/src/main/java/androidx/tv/foundation/lazy/list/LazyListAnimateScrollScope.kt
+++ b/tv/tv-foundation/src/main/java/androidx/tv/foundation/lazy/list/LazyListAnimateScrollScope.kt
@@ -21,6 +21,7 @@
 import androidx.compose.ui.util.fastFirstOrNull
 import androidx.compose.ui.util.fastSumBy
 import androidx.tv.foundation.lazy.layout.LazyAnimateScrollScope
+import kotlin.math.abs
 
 internal class LazyListAnimateScrollScope(
     private val state: TvLazyListState
@@ -52,8 +53,10 @@
         val visibleItems = state.layoutInfo.visibleItemsInfo
         val averageSize = visibleItems.fastSumBy { it.size } / visibleItems.size
         val indexesDiff = index - firstVisibleItemIndex
+        var coercedOffset = minOf(abs(targetScrollOffset), averageSize)
+        if (targetScrollOffset < 0) coercedOffset *= -1
         return (averageSize * indexesDiff).toFloat() +
-            targetScrollOffset - firstVisibleItemScrollOffset
+            coercedOffset - firstVisibleItemScrollOffset
     }
 
     override suspend fun scroll(block: suspend ScrollScope.() -> Unit) {
diff --git a/tv/tv-foundation/src/main/java/androidx/tv/foundation/lazy/list/LazyListPinnableContainerProvider.kt b/tv/tv-foundation/src/main/java/androidx/tv/foundation/lazy/list/LazyListPinnableContainerProvider.kt
index 3056d9d..4ae5108 100644
--- a/tv/tv-foundation/src/main/java/androidx/tv/foundation/lazy/list/LazyListPinnableContainerProvider.kt
+++ b/tv/tv-foundation/src/main/java/androidx/tv/foundation/lazy/list/LazyListPinnableContainerProvider.kt
@@ -56,7 +56,7 @@
 
     /**
      * It is a valid use case when users of this class call [pin] multiple times individually,
-     * so we want to do the unpinning only when all of the users called [unpin].
+     * so we want to do the unpinning only when all of the users called [release].
      */
     private var pinsCount by mutableStateOf(0)
 
@@ -77,7 +77,7 @@
                 if (value !== previous) {
                     _parentPinnableContainer = value
                     if (pinsCount > 0) {
-                        parentHandle?.unpin()
+                        parentHandle?.release()
                         parentHandle = value?.pin()
                     }
                 }
@@ -93,19 +93,19 @@
         return this
     }
 
-    override fun unpin() {
-        check(pinsCount > 0) { "Unpin should only be called once" }
+    override fun release() {
+        check(pinsCount > 0) { "Release should only be called once" }
         pinsCount--
         if (pinsCount == 0) {
             state.pinnedItems.remove(this)
-            parentHandle?.unpin()
+            parentHandle?.release()
             parentHandle = null
         }
     }
 
     fun onDisposed() {
         repeat(pinsCount) {
-            unpin()
+            release()
         }
     }
 }
diff --git a/tv/tv-material/api/current.txt b/tv/tv-material/api/current.txt
index 8151caa..3e782bc 100644
--- a/tv/tv-material/api/current.txt
+++ b/tv/tv-material/api/current.txt
@@ -65,6 +65,11 @@
   public final class ShapesKt {
   }
 
+  public final class SurfaceKt {
+    method public static androidx.compose.runtime.ProvidableCompositionLocal<androidx.compose.ui.unit.Dp> getLocalAbsoluteTonalElevation();
+    property public static final androidx.compose.runtime.ProvidableCompositionLocal<androidx.compose.ui.unit.Dp> LocalAbsoluteTonalElevation;
+  }
+
   public final class TabKt {
   }
 
diff --git a/tv/tv-material/api/public_plus_experimental_current.txt b/tv/tv-material/api/public_plus_experimental_current.txt
index 69a09ad..cc8da19 100644
--- a/tv/tv-material/api/public_plus_experimental_current.txt
+++ b/tv/tv-material/api/public_plus_experimental_current.txt
@@ -188,6 +188,12 @@
   public final class ShapesKt {
   }
 
+  public final class SurfaceKt {
+    method @androidx.compose.runtime.Composable @androidx.compose.runtime.NonRestartableComposable @androidx.tv.material3.ExperimentalTvMaterial3Api public static void Surface(boolean selected, kotlin.jvm.functions.Function0<kotlin.Unit> onClick, optional androidx.compose.ui.Modifier modifier, optional boolean enabled, optional androidx.compose.ui.graphics.Shape shape, optional long color, optional long contentColor, optional float tonalElevation, optional float shadowElevation, optional androidx.compose.foundation.BorderStroke? border, optional androidx.compose.foundation.interaction.MutableInteractionSource interactionSource, kotlin.jvm.functions.Function0<kotlin.Unit> content);
+    method public static androidx.compose.runtime.ProvidableCompositionLocal<androidx.compose.ui.unit.Dp> getLocalAbsoluteTonalElevation();
+    property public static final androidx.compose.runtime.ProvidableCompositionLocal<androidx.compose.ui.unit.Dp> LocalAbsoluteTonalElevation;
+  }
+
   @androidx.tv.material3.ExperimentalTvMaterial3Api public final class TabColors {
   }
 
diff --git a/tv/tv-material/api/restricted_current.txt b/tv/tv-material/api/restricted_current.txt
index 8151caa..3e782bc 100644
--- a/tv/tv-material/api/restricted_current.txt
+++ b/tv/tv-material/api/restricted_current.txt
@@ -65,6 +65,11 @@
   public final class ShapesKt {
   }
 
+  public final class SurfaceKt {
+    method public static androidx.compose.runtime.ProvidableCompositionLocal<androidx.compose.ui.unit.Dp> getLocalAbsoluteTonalElevation();
+    property public static final androidx.compose.runtime.ProvidableCompositionLocal<androidx.compose.ui.unit.Dp> LocalAbsoluteTonalElevation;
+  }
+
   public final class TabKt {
   }
 
diff --git a/tv/tv-material/src/androidTest/java/androidx/tv/material3/SurfaceTest.kt b/tv/tv-material/src/androidTest/java/androidx/tv/material3/SurfaceTest.kt
new file mode 100644
index 0000000..a5c80ea
--- /dev/null
+++ b/tv/tv-material/src/androidTest/java/androidx/tv/material3/SurfaceTest.kt
@@ -0,0 +1,452 @@
+/*
+ * Copyright 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+@file:OptIn(ExperimentalTvMaterial3Api::class)
+
+package androidx.tv.material3
+
+import android.os.Build
+import androidx.compose.foundation.clickable
+import androidx.compose.foundation.layout.Box
+import androidx.compose.foundation.layout.Column
+import androidx.compose.foundation.layout.Spacer
+import androidx.compose.foundation.layout.fillMaxSize
+import androidx.compose.foundation.layout.padding
+import androidx.compose.foundation.layout.size
+import androidx.compose.foundation.shape.CircleShape
+import androidx.compose.runtime.Composable
+import androidx.compose.runtime.CompositionLocalProvider
+import androidx.compose.runtime.mutableStateOf
+import androidx.compose.testutils.assertPixels
+import androidx.compose.testutils.assertShape
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.composed
+import androidx.compose.ui.geometry.Offset
+import androidx.compose.ui.graphics.Color
+import androidx.compose.ui.graphics.RectangleShape
+import androidx.compose.ui.graphics.asAndroidBitmap
+import androidx.compose.ui.platform.testTag
+import androidx.compose.ui.semantics.Role
+import androidx.compose.ui.semantics.SemanticsProperties
+import androidx.compose.ui.semantics.role
+import androidx.compose.ui.semantics.semantics
+import androidx.compose.ui.test.SemanticsMatcher
+import androidx.compose.ui.test.assert
+import androidx.compose.ui.test.assertHasClickAction
+import androidx.compose.ui.test.assertIsEnabled
+import androidx.compose.ui.test.assertTextEquals
+import androidx.compose.ui.test.captureToImage
+import androidx.compose.ui.test.click
+import androidx.compose.ui.test.junit4.ComposeContentTestRule
+import androidx.compose.ui.test.junit4.createComposeRule
+import androidx.compose.ui.test.onNodeWithTag
+import androidx.compose.ui.test.performClick
+import androidx.compose.ui.test.performTouchInput
+import androidx.compose.ui.unit.Dp
+import androidx.compose.ui.unit.dp
+import androidx.test.ext.junit.runners.AndroidJUnit4
+import androidx.test.filters.MediumTest
+import androidx.test.filters.SdkSuppress
+import com.google.common.truth.Truth
+import org.junit.Rule
+import org.junit.Test
+import org.junit.runner.RunWith
+
+@MediumTest
+@RunWith(AndroidJUnit4::class)
+class SurfaceTest {
+
+    @get:Rule
+    val rule = createComposeRule()
+
+    @SdkSuppress(minSdkVersion = Build.VERSION_CODES.O)
+    @Test
+    fun noTonalElevationColorIsSetOnNonElevatedSurfaceColor() {
+        var absoluteTonalElevation: Dp = 0.dp
+        var surfaceColor: Color = Color.Unspecified
+        rule.setMaterialContent(lightColorScheme()) {
+            surfaceColor = MaterialTheme.colorScheme.surface
+            Box(
+                Modifier
+                    .size(10.dp, 10.dp)
+                    .semantics(mergeDescendants = true) {}
+                    .testTag("box")
+            ) {
+                Surface(
+                    color = surfaceColor,
+                    tonalElevation = 0.dp,
+                    selected = false,
+                    onClick = {}
+                ) {
+                    absoluteTonalElevation = LocalAbsoluteTonalElevation.current
+                    Box(Modifier.fillMaxSize())
+                }
+            }
+        }
+
+        rule.runOnIdle {
+            Truth.assertThat(absoluteTonalElevation).isEqualTo(0.dp)
+        }
+
+        rule.onNodeWithTag("box")
+            .captureToImage()
+            .assertShape(
+                density = rule.density,
+                shape = RectangleShape,
+                shapeColor = surfaceColor,
+                backgroundColor = Color.White
+            )
+    }
+
+    @SdkSuppress(minSdkVersion = Build.VERSION_CODES.O)
+    @Test
+    fun tonalElevationColorIsSetOnElevatedSurfaceColor() {
+        var absoluteTonalElevation: Dp = 0.dp
+        var surfaceTonalColor: Color = Color.Unspecified
+        var surfaceColor: Color
+        rule.setMaterialContent(lightColorScheme()) {
+            surfaceColor = MaterialTheme.colorScheme.surface
+            Box(
+                Modifier
+                    .size(10.dp, 10.dp)
+                    .semantics(mergeDescendants = true) {}
+                    .testTag("box")
+            ) {
+                Surface(
+                    color = surfaceColor,
+                    tonalElevation = 2.dp,
+                    selected = false,
+                    onClick = {}
+                ) {
+                    absoluteTonalElevation = LocalAbsoluteTonalElevation.current
+                    Box(Modifier.fillMaxSize())
+                }
+                surfaceTonalColor =
+                    MaterialTheme.colorScheme.surfaceColorAtElevation(absoluteTonalElevation)
+            }
+        }
+
+        rule.runOnIdle {
+            Truth.assertThat(absoluteTonalElevation).isEqualTo(2.dp)
+        }
+
+        rule.onNodeWithTag("box")
+            .captureToImage()
+            .assertShape(
+                density = rule.density,
+                shape = RectangleShape,
+                shapeColor = surfaceTonalColor,
+                backgroundColor = Color.White
+            )
+    }
+
+    @SdkSuppress(minSdkVersion = Build.VERSION_CODES.O)
+    @Test
+    fun tonalElevationColorIsNotSetOnNonSurfaceColor() {
+        var absoluteTonalElevation: Dp = 0.dp
+        rule.setMaterialContent(lightColorScheme()) {
+            Box(
+                Modifier
+                    .size(10.dp, 10.dp)
+                    .semantics(mergeDescendants = true) {}
+                    .testTag("box")
+            ) {
+                Surface(
+                    color = Color.Green,
+                    tonalElevation = 2.dp,
+                    selected = false,
+                    onClick = {}
+                ) {
+                    Box(Modifier.fillMaxSize())
+                    absoluteTonalElevation = LocalAbsoluteTonalElevation.current
+                }
+            }
+        }
+
+        rule.runOnIdle {
+            Truth.assertThat(absoluteTonalElevation).isEqualTo(2.dp)
+        }
+
+        rule.onNodeWithTag("box")
+            .captureToImage()
+            .assertShape(
+                density = rule.density,
+                shape = RectangleShape,
+                shapeColor = Color.Green,
+                backgroundColor = Color.White
+            )
+    }
+
+    @Test
+    fun absoluteElevationCompositionLocalIsSet() {
+        var outerElevation: Dp? = null
+        var innerElevation: Dp? = null
+        rule.setMaterialContent(lightColorScheme()) {
+            Surface(
+                tonalElevation = 2.dp,
+                selected = false,
+                onClick = {}
+            ) {
+                outerElevation = LocalAbsoluteTonalElevation.current
+                Surface(
+                    tonalElevation = 4.dp,
+                    selected = false,
+                    onClick = {}
+                ) {
+                    innerElevation = LocalAbsoluteTonalElevation.current
+                }
+            }
+        }
+
+        rule.runOnIdle {
+            Truth.assertThat(outerElevation).isEqualTo(2.dp)
+            Truth.assertThat(innerElevation).isEqualTo(6.dp)
+        }
+    }
+
+    @SdkSuppress(minSdkVersion = Build.VERSION_CODES.O)
+    @Test
+    fun absoluteElevationIsNotUsedForShadows() {
+        rule.setMaterialContent(lightColorScheme()) {
+            Column {
+                Box(
+                    Modifier
+                        .padding(10.dp)
+                        .size(10.dp, 10.dp)
+                        .semantics(mergeDescendants = true) {}
+                        .testTag("top level")
+                ) {
+                    Surface(
+                        modifier = Modifier
+                            .fillMaxSize()
+                            .padding(0.dp),
+                        tonalElevation = 2.dp,
+                        shadowElevation = 2.dp,
+                        color = Color.Blue,
+                        content = {},
+                        selected = false,
+                        onClick = {}
+                    )
+                }
+
+                // Set LocalAbsoluteTonalElevation to increase the absolute elevation
+                CompositionLocalProvider(
+                    LocalAbsoluteTonalElevation provides 2.dp
+                ) {
+                    Box(
+                        Modifier
+                            .padding(10.dp)
+                            .size(10.dp, 10.dp)
+                            .semantics(mergeDescendants = true) {}
+                            .testTag("nested")
+                    ) {
+                        Surface(
+                            modifier = Modifier
+                                .fillMaxSize()
+                                .padding(0.dp),
+                            tonalElevation = 0.dp,
+                            shadowElevation = 2.dp,
+                            color = Color.Blue,
+                            content = {},
+                            selected = false,
+                            onClick = {}
+                        )
+                    }
+                }
+            }
+        }
+
+        val topLevelSurfaceBitmap = rule.onNodeWithTag("top level").captureToImage()
+        val nestedSurfaceBitmap = rule.onNodeWithTag("nested").captureToImage()
+            .asAndroidBitmap()
+
+        topLevelSurfaceBitmap.assertPixels {
+            Color(nestedSurfaceBitmap.getPixel(it.x, it.y))
+        }
+    }
+
+    /**
+     * Tests that composed modifiers applied to Surface are applied within the changes to
+     * [LocalContentColor], so they can consume the updated values.
+     */
+    @Test
+    fun contentColorSetBeforeModifier() {
+        var contentColor: Color = Color.Unspecified
+        val expectedColor = Color.Blue
+        rule.setMaterialContent(lightColorScheme()) {
+            CompositionLocalProvider(LocalContentColor provides Color.Red) {
+                Surface(
+                    modifier = Modifier.composed {
+                        contentColor = LocalContentColor.current
+                        Modifier
+                    },
+                    tonalElevation = 2.dp,
+                    contentColor = expectedColor,
+                    content = {},
+                    selected = false,
+                    onClick = {}
+                )
+            }
+        }
+
+        rule.runOnIdle {
+            Truth.assertThat(contentColor).isEqualTo(expectedColor)
+        }
+    }
+
+    @Test
+    fun surface_blockClicksBehind() {
+        val state = mutableStateOf(0)
+        rule.setContent {
+            Box(Modifier.fillMaxSize()) {
+                Box(
+                    modifier = Modifier
+                        .fillMaxSize()
+                        .testTag("clickable")
+                        .clickable { state.value += 1 },
+                ) { Text("button fullscreen") }
+                Surface(
+                    modifier = Modifier
+                        .fillMaxSize()
+                        .testTag("surface"),
+                    onClick = {},
+                    selected = false
+                ) {}
+            }
+        }
+        rule.onNodeWithTag("clickable").assertHasClickAction().performClick()
+        // still 0
+        Truth.assertThat(state.value).isEqualTo(0)
+    }
+
+    @Test
+    fun selectable_semantics() {
+        val selected = mutableStateOf(false)
+        rule.setMaterialContent(lightColorScheme()) {
+            Surface(
+                selected = selected.value,
+                onClick = { selected.value = !selected.value },
+                modifier = Modifier.testTag("surface"),
+            ) {
+                Text("${selected.value}")
+                Spacer(Modifier.size(30.dp))
+            }
+        }
+        rule.onNodeWithTag("surface")
+            .assertHasClickAction()
+            .assert(SemanticsMatcher.expectValue(SemanticsProperties.Role, Role.Tab))
+            .assertIsEnabled()
+            // since we merge descendants we should have text on the same node
+            .assertTextEquals("false")
+            .performClick()
+            .assertTextEquals("true")
+    }
+
+    @Test
+    fun selectable_customSemantics() {
+        val selected = mutableStateOf(false)
+        rule.setMaterialContent(lightColorScheme()) {
+            Surface(
+                selected = selected.value,
+                onClick = { selected.value = !selected.value },
+                modifier = Modifier
+                    .semantics { role = Role.Switch }
+                    .testTag("surface"),
+            ) {
+                Text("${selected.value}")
+                Spacer(Modifier.size(30.dp))
+            }
+        }
+        rule.onNodeWithTag("surface")
+            .assertHasClickAction()
+            .assert(SemanticsMatcher.expectValue(SemanticsProperties.Role, Role.Switch))
+            .assertIsEnabled()
+            // since we merge descendants we should have text on the same node
+            .assertTextEquals("false")
+            .performClick()
+            .assertTextEquals("true")
+    }
+
+    @Test
+    fun selectable_clickAction() {
+        val selected = mutableStateOf(false)
+        rule.setMaterialContent(lightColorScheme()) {
+            Surface(
+                selected = selected.value,
+                onClick = { selected.value = !selected.value },
+                modifier = Modifier.testTag("surface")
+            ) { Spacer(Modifier.size(30.dp)) }
+        }
+        rule.onNodeWithTag("surface").performClick()
+        Truth.assertThat(selected.value).isTrue()
+
+        rule.onNodeWithTag("surface").performClick()
+        Truth.assertThat(selected.value).isFalse()
+    }
+
+    @Test
+    fun selectable_clickOutsideShapeBounds() {
+        val selected = mutableStateOf(false)
+        rule.setMaterialContent(lightColorScheme()) {
+            Surface(
+                selected = selected.value,
+                onClick = { selected.value = !selected.value },
+                modifier = Modifier.testTag("surface"),
+                shape = CircleShape
+            ) { Spacer(Modifier.size(100.dp)) }
+        }
+        // Click inside the circular shape bounds. Expecting a selection change.
+        rule.onNodeWithTag("surface").performClick()
+        Truth.assertThat(selected.value).isTrue()
+
+        // Click outside the circular shape bounds. Expecting a selection to stay as it.
+        rule.onNodeWithTag("surface").performTouchInput { click(Offset(10f, 10f)) }
+        Truth.assertThat(selected.value).isTrue()
+    }
+
+    @Test
+    fun selectable_smallTouchTarget_clickOutsideShapeBounds() {
+        val selected = mutableStateOf(false)
+        rule.setMaterialContent(lightColorScheme()) {
+            Surface(
+                selected = selected.value,
+                onClick = { selected.value = !selected.value },
+                modifier = Modifier.testTag("surface"),
+                shape = CircleShape
+            ) { Spacer(Modifier.size(40.dp)) }
+        }
+        // Click inside the circular shape bounds. Expecting a selection change.
+        rule.onNodeWithTag("surface").performClick()
+        Truth.assertThat(selected.value).isTrue()
+
+        // Click outside the circular shape bounds. Still expecting a selection change, as the
+        // touch target has a minimum size of 48dp.
+        rule.onNodeWithTag("surface").performTouchInput { click(Offset(2f, 2f)) }
+        Truth.assertThat(selected.value).isFalse()
+    }
+
+    private fun ComposeContentTestRule.setMaterialContent(
+        colorScheme: ColorScheme = lightColorScheme(),
+        content: @Composable () -> Unit
+    ) {
+        setContent {
+            MaterialTheme(
+                colorScheme = colorScheme,
+                content = content
+            )
+        }
+    }
+}
diff --git a/tv/tv-material/src/main/java/androidx/tv/material3/Surface.kt b/tv/tv-material/src/main/java/androidx/tv/material3/Surface.kt
new file mode 100644
index 0000000..67aa138
--- /dev/null
+++ b/tv/tv-material/src/main/java/androidx/tv/material3/Surface.kt
@@ -0,0 +1,177 @@
+/*
+ * Copyright 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+@file:OptIn(ExperimentalTvMaterial3Api::class)
+
+package androidx.tv.material3
+
+import androidx.compose.foundation.BorderStroke
+import androidx.compose.foundation.background
+import androidx.compose.foundation.border
+import androidx.compose.foundation.interaction.Interaction
+import androidx.compose.foundation.interaction.MutableInteractionSource
+import androidx.compose.foundation.interaction.PressInteraction
+import androidx.compose.foundation.layout.Box
+import androidx.compose.foundation.selection.selectable
+import androidx.compose.runtime.Composable
+import androidx.compose.runtime.CompositionLocalProvider
+import androidx.compose.runtime.NonRestartableComposable
+import androidx.compose.runtime.compositionLocalOf
+import androidx.compose.runtime.remember
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.draw.clip
+import androidx.compose.ui.draw.shadow
+import androidx.compose.ui.graphics.Color
+import androidx.compose.ui.graphics.RectangleShape
+import androidx.compose.ui.graphics.Shape
+import androidx.compose.ui.semantics.Role
+import androidx.compose.ui.semantics.semantics
+import androidx.compose.ui.unit.Dp
+import androidx.compose.ui.unit.dp
+
+/**
+ * Material surface is the central metaphor in material design. Each surface exists at a given
+ * elevation, which influences how that piece of surface visually relates to other surfaces and how
+ * that surface is modified by tonal variance.
+ *
+ * This version of Surface is responsible for a selection handling as well as everything else that
+ * a regular Surface does:
+ *
+ * This selectable Surface is responsible for:
+ *
+ * 1) Clipping: Surface clips its children to the shape specified by [shape]
+ *
+ * 2) Borders: If [shape] has a border, then it will also be drawn.
+ *
+ * 3) Background: Surface fills the shape specified by [shape] with the [color]. If [color] is
+ * [ColorScheme.surface] a color overlay may be applied. The color of the overlay depends on the
+ * [tonalElevation] of this Surface, and the [LocalAbsoluteTonalElevation] set by any
+ * parent surfaces. This ensures that a Surface never appears to have a lower elevation overlay than
+ * its ancestors, by summing the elevation of all previous Surfaces.
+ *
+ * 4) Content color: Surface uses [contentColor] to specify a preferred color for the content of
+ * this surface - this is used by the [Text] and Icon components as a default color. If no
+ * [contentColor] is set, this surface will try and match its background color to a color defined in
+ * the theme [ColorScheme], and return the corresponding content color. For example, if the [color]
+ * of this surface is [ColorScheme.surface], [contentColor] will be set to [ColorScheme.onSurface].
+ * If [color] is not part of the theme palette, [contentColor] will keep the same value set above
+ * this Surface.
+ *
+ * 5) Click handling. This version of surface will react to the clicks, calling [onClick] lambda,
+ * and updating the [interactionSource] when [PressInteraction] occurs.
+ *
+ * 6) Semantics for selection. Just like with [Modifier.selectable], selectable version of Surface
+ * will produce semantics to indicate that it is selected. Also, by default, accessibility services
+ * will describe the element as [Role.Tab]. You may change this by passing a desired [Role] with a
+ * [Modifier.semantics].
+ *
+ * To manually retrieve the content color inside a surface, use [LocalContentColor].
+ *
+ * @param selected whether or not this Surface is selected
+ * @param onClick callback to be called when the surface is clicked
+ * @param modifier Modifier to be applied to the layout corresponding to the surface
+ * @param enabled Controls the enabled state of the surface. When `false`, this surface will not be
+ * clickable
+ * @param shape Defines the surface's shape as well its shadow. A shadow is only displayed if the
+ * [tonalElevation] is greater than zero.
+ * @param color The background color. Use [Color.Transparent] to have no color.
+ * @param contentColor The preferred content color provided by this Surface to its children.
+ * Defaults to either the matching content color for [color], or if [color] is not a color from the
+ * theme, this will keep the same value set above this Surface.
+ * @param border Optional border to draw on top of the surface
+ * @param tonalElevation When [color] is [ColorScheme.surface], a higher the elevation will result
+ * in a darker color in light theme and lighter color in dark theme.
+ * @param shadowElevation The size of the shadow below the surface. Note that It will not affect z
+ * index of the Surface. If you want to change the drawing order you can use `Modifier.zIndex`.
+ * @param interactionSource the [MutableInteractionSource] representing the stream of [Interaction]s
+ * for this Surface. You can create and pass in your own remembered [MutableInteractionSource] if
+ * you want to observe [Interaction]s and customize the appearance / behavior of this Surface in
+ * different [Interaction]s.
+ * @param content the composable content to be displayed inside the surface
+ */
+@ExperimentalTvMaterial3Api
+@Composable
+@NonRestartableComposable
+fun Surface(
+    selected: Boolean,
+    onClick: () -> Unit,
+    modifier: Modifier = Modifier,
+    enabled: Boolean = true,
+    shape: Shape = RectangleShape,
+    color: Color = MaterialTheme.colorScheme.surface,
+    contentColor: Color = contentColorFor(color),
+    tonalElevation: Dp = 0.dp,
+    shadowElevation: Dp = 0.dp,
+    border: BorderStroke? = null,
+    interactionSource: MutableInteractionSource = remember { MutableInteractionSource() },
+    content: @Composable () -> Unit
+) {
+    val absoluteElevation = LocalAbsoluteTonalElevation.current + tonalElevation
+    CompositionLocalProvider(
+        LocalContentColor provides contentColor,
+        LocalAbsoluteTonalElevation provides absoluteElevation
+    ) {
+        Box(
+            modifier = modifier
+                .surface(
+                    shape = shape,
+                    backgroundColor = surfaceColorAtElevation(
+                        color = color,
+                        elevation = absoluteElevation
+                    ),
+                    border = border,
+                    shadowElevation = shadowElevation
+                )
+                .selectable(
+                    selected = selected,
+                    interactionSource = interactionSource,
+                    indication = null,
+                    enabled = enabled,
+                    role = Role.Tab,
+                    onClick = onClick
+                ),
+            propagateMinConstraints = true
+        ) {
+            content()
+        }
+    }
+}
+
+private fun Modifier.surface(
+    shape: Shape,
+    backgroundColor: Color,
+    border: BorderStroke?,
+    shadowElevation: Dp
+) = this.shadow(shadowElevation, shape, clip = false)
+    .then(if (border != null) Modifier.border(border, shape) else Modifier)
+    .background(color = backgroundColor, shape = shape)
+    .clip(shape)
+
+@Composable
+private fun surfaceColorAtElevation(color: Color, elevation: Dp): Color {
+    return if (color == MaterialTheme.colorScheme.surface) {
+        MaterialTheme.colorScheme.surfaceColorAtElevation(elevation)
+    } else {
+        color
+    }
+}
+
+/**
+ * CompositionLocal containing the current absolute elevation provided by [Surface] components. This
+ * absolute elevation is a sum of all the previous elevations. Absolute elevation is only used for
+ * calculating surface tonal colors, and is *not* used for drawing the shadow in a [Surface].
+ */
+val LocalAbsoluteTonalElevation = compositionLocalOf { 0.dp }
diff --git a/wear/protolayout/protolayout-expression/api/current.txt b/wear/protolayout/protolayout-expression/api/current.txt
index 62c253c..7b81823 100644
--- a/wear/protolayout/protolayout-expression/api/current.txt
+++ b/wear/protolayout/protolayout-expression/api/current.txt
@@ -41,6 +41,8 @@
   }
 
   public static interface AnimationParameterBuilders.Easing {
+    method public static androidx.wear.protolayout.expression.AnimationParameterBuilders.Easing fromByteArray(byte[]);
+    method public default byte[] toEasingByteArray();
   }
 
   public static class AnimationParameterBuilders.EasingFunctions {
@@ -81,10 +83,12 @@
   public static interface DynamicBuilders.DynamicBool extends androidx.wear.protolayout.expression.DynamicBuilders.DynamicType {
     method public default androidx.wear.protolayout.expression.DynamicBuilders.DynamicBool and(androidx.wear.protolayout.expression.DynamicBuilders.DynamicBool);
     method public static androidx.wear.protolayout.expression.DynamicBuilders.DynamicBool constant(boolean);
+    method public static androidx.wear.protolayout.expression.DynamicBuilders.DynamicBool fromByteArray(byte[]);
     method public static androidx.wear.protolayout.expression.DynamicBuilders.DynamicBool fromState(String);
     method public default androidx.wear.protolayout.expression.DynamicBuilders.DynamicBool isFalse();
     method public default androidx.wear.protolayout.expression.DynamicBuilders.DynamicBool isTrue();
     method public default androidx.wear.protolayout.expression.DynamicBuilders.DynamicBool or(androidx.wear.protolayout.expression.DynamicBuilders.DynamicBool);
+    method public default byte[] toDynamicBoolByteArray();
   }
 
   public static interface DynamicBuilders.DynamicColor extends androidx.wear.protolayout.expression.DynamicBuilders.DynamicType {
@@ -95,7 +99,9 @@
     method public default androidx.wear.protolayout.expression.DynamicBuilders.DynamicColor animate(androidx.wear.protolayout.expression.AnimationParameterBuilders.AnimationSpec);
     method public default androidx.wear.protolayout.expression.DynamicBuilders.DynamicColor animate();
     method public static androidx.wear.protolayout.expression.DynamicBuilders.DynamicColor constant(@ColorInt int);
+    method public static androidx.wear.protolayout.expression.DynamicBuilders.DynamicColor fromByteArray(byte[]);
     method public static androidx.wear.protolayout.expression.DynamicBuilders.DynamicColor fromState(String);
+    method public default byte[] toDynamicColorByteArray();
   }
 
   public static interface DynamicBuilders.DynamicFloat extends androidx.wear.protolayout.expression.DynamicBuilders.DynamicType {
@@ -109,7 +115,9 @@
     method public static androidx.wear.protolayout.expression.DynamicBuilders.DynamicFloat constant(float);
     method public default androidx.wear.protolayout.expression.DynamicBuilders.DynamicString format();
     method public default androidx.wear.protolayout.expression.DynamicBuilders.DynamicString format(androidx.wear.protolayout.expression.DynamicBuilders.DynamicFloat.FloatFormatter);
+    method public static androidx.wear.protolayout.expression.DynamicBuilders.DynamicFloat fromByteArray(byte[]);
     method public static androidx.wear.protolayout.expression.DynamicBuilders.DynamicFloat fromState(String);
+    method public default byte[] toDynamicFloatByteArray();
   }
 
   public static class DynamicBuilders.DynamicFloat.FloatFormatter {
@@ -125,7 +133,9 @@
     method public static androidx.wear.protolayout.expression.DynamicBuilders.DynamicInt32 constant(int);
     method public default androidx.wear.protolayout.expression.DynamicBuilders.DynamicString format();
     method public default androidx.wear.protolayout.expression.DynamicBuilders.DynamicString format(androidx.wear.protolayout.expression.DynamicBuilders.DynamicInt32.IntFormatter);
+    method public static androidx.wear.protolayout.expression.DynamicBuilders.DynamicInt32 fromByteArray(byte[]);
     method public static androidx.wear.protolayout.expression.DynamicBuilders.DynamicInt32 fromState(String);
+    method public default byte[] toDynamicInt32ByteArray();
   }
 
   public static class DynamicBuilders.DynamicInt32.IntFormatter {
@@ -137,8 +147,10 @@
   public static interface DynamicBuilders.DynamicString extends androidx.wear.protolayout.expression.DynamicBuilders.DynamicType {
     method public default androidx.wear.protolayout.expression.DynamicBuilders.DynamicString concat(androidx.wear.protolayout.expression.DynamicBuilders.DynamicString);
     method public static androidx.wear.protolayout.expression.DynamicBuilders.DynamicString constant(String);
+    method public static androidx.wear.protolayout.expression.DynamicBuilders.DynamicString fromByteArray(byte[]);
     method public static androidx.wear.protolayout.expression.DynamicBuilders.DynamicString fromState(String);
     method public static androidx.wear.protolayout.expression.ConditionScopes.ConditionScope<androidx.wear.protolayout.expression.DynamicBuilders.DynamicString!,java.lang.String!> onCondition(androidx.wear.protolayout.expression.DynamicBuilders.DynamicBool);
+    method public default byte[] toDynamicStringByteArray();
   }
 
   public static interface DynamicBuilders.DynamicType {
diff --git a/wear/protolayout/protolayout-expression/api/public_plus_experimental_current.txt b/wear/protolayout/protolayout-expression/api/public_plus_experimental_current.txt
index 4b4ade8..a8b4bed 100644
--- a/wear/protolayout/protolayout-expression/api/public_plus_experimental_current.txt
+++ b/wear/protolayout/protolayout-expression/api/public_plus_experimental_current.txt
@@ -41,6 +41,8 @@
   }
 
   public static interface AnimationParameterBuilders.Easing {
+    method public static androidx.wear.protolayout.expression.AnimationParameterBuilders.Easing fromByteArray(byte[]);
+    method public default byte[] toEasingByteArray();
   }
 
   public static class AnimationParameterBuilders.EasingFunctions {
@@ -81,10 +83,12 @@
   public static interface DynamicBuilders.DynamicBool extends androidx.wear.protolayout.expression.DynamicBuilders.DynamicType {
     method public default androidx.wear.protolayout.expression.DynamicBuilders.DynamicBool and(androidx.wear.protolayout.expression.DynamicBuilders.DynamicBool);
     method public static androidx.wear.protolayout.expression.DynamicBuilders.DynamicBool constant(boolean);
+    method public static androidx.wear.protolayout.expression.DynamicBuilders.DynamicBool fromByteArray(byte[]);
     method public static androidx.wear.protolayout.expression.DynamicBuilders.DynamicBool fromState(String);
     method public default androidx.wear.protolayout.expression.DynamicBuilders.DynamicBool isFalse();
     method public default androidx.wear.protolayout.expression.DynamicBuilders.DynamicBool isTrue();
     method public default androidx.wear.protolayout.expression.DynamicBuilders.DynamicBool or(androidx.wear.protolayout.expression.DynamicBuilders.DynamicBool);
+    method public default byte[] toDynamicBoolByteArray();
   }
 
   public static interface DynamicBuilders.DynamicColor extends androidx.wear.protolayout.expression.DynamicBuilders.DynamicType {
@@ -95,7 +99,9 @@
     method public default androidx.wear.protolayout.expression.DynamicBuilders.DynamicColor animate(androidx.wear.protolayout.expression.AnimationParameterBuilders.AnimationSpec);
     method public default androidx.wear.protolayout.expression.DynamicBuilders.DynamicColor animate();
     method public static androidx.wear.protolayout.expression.DynamicBuilders.DynamicColor constant(@ColorInt int);
+    method public static androidx.wear.protolayout.expression.DynamicBuilders.DynamicColor fromByteArray(byte[]);
     method public static androidx.wear.protolayout.expression.DynamicBuilders.DynamicColor fromState(String);
+    method public default byte[] toDynamicColorByteArray();
   }
 
   public static interface DynamicBuilders.DynamicFloat extends androidx.wear.protolayout.expression.DynamicBuilders.DynamicType {
@@ -109,7 +115,9 @@
     method public static androidx.wear.protolayout.expression.DynamicBuilders.DynamicFloat constant(float);
     method public default androidx.wear.protolayout.expression.DynamicBuilders.DynamicString format();
     method public default androidx.wear.protolayout.expression.DynamicBuilders.DynamicString format(androidx.wear.protolayout.expression.DynamicBuilders.DynamicFloat.FloatFormatter);
+    method public static androidx.wear.protolayout.expression.DynamicBuilders.DynamicFloat fromByteArray(byte[]);
     method public static androidx.wear.protolayout.expression.DynamicBuilders.DynamicFloat fromState(String);
+    method public default byte[] toDynamicFloatByteArray();
   }
 
   public static class DynamicBuilders.DynamicFloat.FloatFormatter {
@@ -125,7 +133,9 @@
     method public static androidx.wear.protolayout.expression.DynamicBuilders.DynamicInt32 constant(int);
     method public default androidx.wear.protolayout.expression.DynamicBuilders.DynamicString format();
     method public default androidx.wear.protolayout.expression.DynamicBuilders.DynamicString format(androidx.wear.protolayout.expression.DynamicBuilders.DynamicInt32.IntFormatter);
+    method public static androidx.wear.protolayout.expression.DynamicBuilders.DynamicInt32 fromByteArray(byte[]);
     method public static androidx.wear.protolayout.expression.DynamicBuilders.DynamicInt32 fromState(String);
+    method public default byte[] toDynamicInt32ByteArray();
   }
 
   public static class DynamicBuilders.DynamicInt32.IntFormatter {
@@ -137,8 +147,10 @@
   public static interface DynamicBuilders.DynamicString extends androidx.wear.protolayout.expression.DynamicBuilders.DynamicType {
     method public default androidx.wear.protolayout.expression.DynamicBuilders.DynamicString concat(androidx.wear.protolayout.expression.DynamicBuilders.DynamicString);
     method public static androidx.wear.protolayout.expression.DynamicBuilders.DynamicString constant(String);
+    method public static androidx.wear.protolayout.expression.DynamicBuilders.DynamicString fromByteArray(byte[]);
     method public static androidx.wear.protolayout.expression.DynamicBuilders.DynamicString fromState(String);
     method public static androidx.wear.protolayout.expression.ConditionScopes.ConditionScope<androidx.wear.protolayout.expression.DynamicBuilders.DynamicString!,java.lang.String!> onCondition(androidx.wear.protolayout.expression.DynamicBuilders.DynamicBool);
+    method public default byte[] toDynamicStringByteArray();
   }
 
   public static interface DynamicBuilders.DynamicType {
diff --git a/wear/protolayout/protolayout-expression/api/restricted_current.txt b/wear/protolayout/protolayout-expression/api/restricted_current.txt
index 62c253c..7b81823 100644
--- a/wear/protolayout/protolayout-expression/api/restricted_current.txt
+++ b/wear/protolayout/protolayout-expression/api/restricted_current.txt
@@ -41,6 +41,8 @@
   }
 
   public static interface AnimationParameterBuilders.Easing {
+    method public static androidx.wear.protolayout.expression.AnimationParameterBuilders.Easing fromByteArray(byte[]);
+    method public default byte[] toEasingByteArray();
   }
 
   public static class AnimationParameterBuilders.EasingFunctions {
@@ -81,10 +83,12 @@
   public static interface DynamicBuilders.DynamicBool extends androidx.wear.protolayout.expression.DynamicBuilders.DynamicType {
     method public default androidx.wear.protolayout.expression.DynamicBuilders.DynamicBool and(androidx.wear.protolayout.expression.DynamicBuilders.DynamicBool);
     method public static androidx.wear.protolayout.expression.DynamicBuilders.DynamicBool constant(boolean);
+    method public static androidx.wear.protolayout.expression.DynamicBuilders.DynamicBool fromByteArray(byte[]);
     method public static androidx.wear.protolayout.expression.DynamicBuilders.DynamicBool fromState(String);
     method public default androidx.wear.protolayout.expression.DynamicBuilders.DynamicBool isFalse();
     method public default androidx.wear.protolayout.expression.DynamicBuilders.DynamicBool isTrue();
     method public default androidx.wear.protolayout.expression.DynamicBuilders.DynamicBool or(androidx.wear.protolayout.expression.DynamicBuilders.DynamicBool);
+    method public default byte[] toDynamicBoolByteArray();
   }
 
   public static interface DynamicBuilders.DynamicColor extends androidx.wear.protolayout.expression.DynamicBuilders.DynamicType {
@@ -95,7 +99,9 @@
     method public default androidx.wear.protolayout.expression.DynamicBuilders.DynamicColor animate(androidx.wear.protolayout.expression.AnimationParameterBuilders.AnimationSpec);
     method public default androidx.wear.protolayout.expression.DynamicBuilders.DynamicColor animate();
     method public static androidx.wear.protolayout.expression.DynamicBuilders.DynamicColor constant(@ColorInt int);
+    method public static androidx.wear.protolayout.expression.DynamicBuilders.DynamicColor fromByteArray(byte[]);
     method public static androidx.wear.protolayout.expression.DynamicBuilders.DynamicColor fromState(String);
+    method public default byte[] toDynamicColorByteArray();
   }
 
   public static interface DynamicBuilders.DynamicFloat extends androidx.wear.protolayout.expression.DynamicBuilders.DynamicType {
@@ -109,7 +115,9 @@
     method public static androidx.wear.protolayout.expression.DynamicBuilders.DynamicFloat constant(float);
     method public default androidx.wear.protolayout.expression.DynamicBuilders.DynamicString format();
     method public default androidx.wear.protolayout.expression.DynamicBuilders.DynamicString format(androidx.wear.protolayout.expression.DynamicBuilders.DynamicFloat.FloatFormatter);
+    method public static androidx.wear.protolayout.expression.DynamicBuilders.DynamicFloat fromByteArray(byte[]);
     method public static androidx.wear.protolayout.expression.DynamicBuilders.DynamicFloat fromState(String);
+    method public default byte[] toDynamicFloatByteArray();
   }
 
   public static class DynamicBuilders.DynamicFloat.FloatFormatter {
@@ -125,7 +133,9 @@
     method public static androidx.wear.protolayout.expression.DynamicBuilders.DynamicInt32 constant(int);
     method public default androidx.wear.protolayout.expression.DynamicBuilders.DynamicString format();
     method public default androidx.wear.protolayout.expression.DynamicBuilders.DynamicString format(androidx.wear.protolayout.expression.DynamicBuilders.DynamicInt32.IntFormatter);
+    method public static androidx.wear.protolayout.expression.DynamicBuilders.DynamicInt32 fromByteArray(byte[]);
     method public static androidx.wear.protolayout.expression.DynamicBuilders.DynamicInt32 fromState(String);
+    method public default byte[] toDynamicInt32ByteArray();
   }
 
   public static class DynamicBuilders.DynamicInt32.IntFormatter {
@@ -137,8 +147,10 @@
   public static interface DynamicBuilders.DynamicString extends androidx.wear.protolayout.expression.DynamicBuilders.DynamicType {
     method public default androidx.wear.protolayout.expression.DynamicBuilders.DynamicString concat(androidx.wear.protolayout.expression.DynamicBuilders.DynamicString);
     method public static androidx.wear.protolayout.expression.DynamicBuilders.DynamicString constant(String);
+    method public static androidx.wear.protolayout.expression.DynamicBuilders.DynamicString fromByteArray(byte[]);
     method public static androidx.wear.protolayout.expression.DynamicBuilders.DynamicString fromState(String);
     method public static androidx.wear.protolayout.expression.ConditionScopes.ConditionScope<androidx.wear.protolayout.expression.DynamicBuilders.DynamicString!,java.lang.String!> onCondition(androidx.wear.protolayout.expression.DynamicBuilders.DynamicBool);
+    method public default byte[] toDynamicStringByteArray();
   }
 
   public static interface DynamicBuilders.DynamicType {
diff --git a/wear/protolayout/protolayout-expression/src/main/java/androidx/wear/protolayout/expression/AnimationParameterBuilders.java b/wear/protolayout/protolayout-expression/src/main/java/androidx/wear/protolayout/expression/AnimationParameterBuilders.java
index 2fb42f3..44bae7c 100644
--- a/wear/protolayout/protolayout-expression/src/main/java/androidx/wear/protolayout/expression/AnimationParameterBuilders.java
+++ b/wear/protolayout/protolayout-expression/src/main/java/androidx/wear/protolayout/expression/AnimationParameterBuilders.java
@@ -25,6 +25,8 @@
 import androidx.annotation.RestrictTo;
 import androidx.annotation.RestrictTo.Scope;
 import androidx.wear.protolayout.expression.proto.AnimationParameterProto;
+import androidx.wear.protolayout.protobuf.ExtensionRegistryLite;
+import androidx.wear.protolayout.protobuf.InvalidProtocolBufferException;
 import java.lang.annotation.Retention;
 import java.lang.annotation.RetentionPolicy;
 
@@ -296,6 +298,24 @@
     @NonNull
     AnimationParameterProto.Easing toEasingProto();
 
+    /** Creates a {@link Easing} from a byte array generated by {@link #toEasingByteArray()}. */
+    @NonNull
+    static Easing fromByteArray(@NonNull byte[] byteArray) {
+      try {
+        return easingFromProto(
+            AnimationParameterProto.Easing.parseFrom(
+                byteArray, ExtensionRegistryLite.getEmptyRegistry()));
+      } catch (InvalidProtocolBufferException e) {
+        throw new IllegalArgumentException("Byte array could not be parsed into Easing", e);
+      }
+    }
+
+    /** Creates a byte array that can later be used with {@link #fromByteArray(byte[])}. */
+    @NonNull
+    default byte[] toEasingByteArray() {
+      return toEasingProto().toByteArray();
+    }
+
     /**
      * Get the fingerprint for this object or null if unknown.
      *
@@ -305,7 +325,8 @@
     @Nullable
     Fingerprint getFingerprint();
 
-    /** Builder to create {@link Easing} objects.
+    /**
+     * Builder to create {@link Easing} objects.
      *
      * @hide
      */
diff --git a/wear/protolayout/protolayout-expression/src/main/java/androidx/wear/protolayout/expression/DynamicBuilders.java b/wear/protolayout/protolayout-expression/src/main/java/androidx/wear/protolayout/expression/DynamicBuilders.java
index 01a67cd..e135c9e 100644
--- a/wear/protolayout/protolayout-expression/src/main/java/androidx/wear/protolayout/expression/DynamicBuilders.java
+++ b/wear/protolayout/protolayout-expression/src/main/java/androidx/wear/protolayout/expression/DynamicBuilders.java
@@ -35,6 +35,9 @@
 import androidx.wear.protolayout.expression.FixedValueBuilders.FixedString;
 import androidx.wear.protolayout.expression.StateEntryBuilders.StateEntryValue;
 import androidx.wear.protolayout.expression.proto.DynamicProto;
+import androidx.wear.protolayout.protobuf.ExtensionRegistryLite;
+import androidx.wear.protolayout.protobuf.InvalidProtocolBufferException;
+
 import java.lang.annotation.Retention;
 import java.lang.annotation.RetentionPolicy;
 
@@ -381,6 +384,27 @@
     @NonNull
     DynamicProto.DynamicInt32 toDynamicInt32Proto();
 
+    /**
+     * Creates a {@link DynamicInt32} from a byte array generated by {@link
+     * #toDynamicInt32ByteArray()}.
+     */
+    @NonNull
+    static DynamicInt32 fromByteArray(@NonNull byte[] byteArray) {
+      try {
+        return dynamicInt32FromProto(
+            DynamicProto.DynamicInt32.parseFrom(
+                byteArray, ExtensionRegistryLite.getEmptyRegistry()));
+      } catch (InvalidProtocolBufferException e) {
+        throw new IllegalArgumentException("Byte array could not be parsed into DynamicInt32", e);
+      }
+    }
+
+    /** Creates a byte array that can later be used with {@link #fromByteArray(byte[])}. */
+    @NonNull
+    default byte[] toDynamicInt32ByteArray() {
+      return toDynamicInt32Proto().toByteArray();
+    }
+
     /** Creates a constant-valued {@link DynamicInt32}. */
     @NonNull
     static DynamicInt32 constant(int constant) {
@@ -481,7 +505,8 @@
     @Nullable
     Fingerprint getFingerprint();
 
-    /** Builder to create {@link DynamicInt32} objects.
+    /**
+     * Builder to create {@link DynamicInt32} objects.
      *
      * @hide
      */
@@ -1183,6 +1208,27 @@
     @NonNull
     DynamicProto.DynamicString toDynamicStringProto();
 
+    /**
+     * Creates a {@link DynamicString} from a byte array generated by {@link
+     * #toDynamicStringByteArray()}.
+     */
+    @NonNull
+    static DynamicString fromByteArray(@NonNull byte[] byteArray) {
+      try {
+        return dynamicStringFromProto(
+            DynamicProto.DynamicString.parseFrom(
+                byteArray, ExtensionRegistryLite.getEmptyRegistry()));
+      } catch (InvalidProtocolBufferException e) {
+        throw new IllegalArgumentException("Byte array could not be parsed into DynamicString", e);
+      }
+    }
+
+    /** Creates a byte array that can later be used with {@link #fromByteArray(byte[])}. */
+    @NonNull
+    default byte[] toDynamicStringByteArray() {
+      return toDynamicStringProto().toByteArray();
+    }
+
     /** Creates a constant-valued {@link DynamicString}. */
     @NonNull
     static DynamicString constant(@NonNull String constant) {
@@ -1242,7 +1288,8 @@
     @Nullable
     Fingerprint getFingerprint();
 
-    /** Builder to create {@link DynamicString} objects.
+    /**
+     * Builder to create {@link DynamicString} objects.
      *
      * @hide
      */
@@ -1704,6 +1751,27 @@
     @NonNull
     DynamicProto.DynamicFloat toDynamicFloatProto();
 
+    /**
+     * Creates a {@link DynamicFloat} from a byte array generated by {@link
+     * #toDynamicFloatByteArray()}.
+     */
+    @NonNull
+    static DynamicFloat fromByteArray(@NonNull byte[] byteArray) {
+      try {
+        return dynamicFloatFromProto(
+            DynamicProto.DynamicFloat.parseFrom(
+                byteArray, ExtensionRegistryLite.getEmptyRegistry()));
+      } catch (InvalidProtocolBufferException e) {
+        throw new IllegalArgumentException("Byte array could not be parsed into DynamicFloat", e);
+      }
+    }
+
+    /** Creates a byte array that can later be used with {@link #fromByteArray(byte[])}. */
+    @NonNull
+    default byte[] toDynamicFloatByteArray() {
+      return toDynamicFloatProto().toByteArray();
+    }
+
     /** Creates a constant-valued {@link DynamicFloat}. */
     @NonNull
     static DynamicFloat constant(float constant) {
@@ -1911,7 +1979,8 @@
     @Nullable
     Fingerprint getFingerprint();
 
-    /** Builder to create {@link DynamicFloat} objects.
+    /**
+     * Builder to create {@link DynamicFloat} objects.
      *
      * @hide
      */
@@ -2259,6 +2328,27 @@
     @NonNull
     DynamicProto.DynamicBool toDynamicBoolProto();
 
+    /**
+     * Creates a {@link DynamicBool} from a byte array generated by {@link
+     * #toDynamicBoolByteArray()}.
+     */
+    @NonNull
+    static DynamicBool fromByteArray(@NonNull byte[] byteArray) {
+      try {
+        return dynamicBoolFromProto(
+            DynamicProto.DynamicBool.parseFrom(
+                byteArray, ExtensionRegistryLite.getEmptyRegistry()));
+      } catch (InvalidProtocolBufferException e) {
+        throw new IllegalArgumentException("Byte array could not be parsed into DynamicBool", e);
+      }
+    }
+
+    /** Creates a byte array that can later be used with {@link #fromByteArray(byte[])}. */
+    @NonNull
+    default byte[] toDynamicBoolByteArray() {
+      return toDynamicBoolProto().toByteArray();
+    }
+
     /** Creates a constant-valued {@link DynamicBool}. */
     @NonNull
     static DynamicBool constant(boolean constant) {
@@ -2330,7 +2420,8 @@
     @Nullable
     Fingerprint getFingerprint();
 
-    /** Builder to create {@link DynamicBool} objects.
+    /**
+     * Builder to create {@link DynamicBool} objects.
      *
      * @hide
      */
@@ -2706,6 +2797,27 @@
     @NonNull
     DynamicProto.DynamicColor toDynamicColorProto();
 
+    /**
+     * Creates a {@link DynamicColor} from a byte array generated by {@link
+     * #toDynamicColorByteArray()}.
+     */
+    @NonNull
+    static DynamicColor fromByteArray(@NonNull byte[] byteArray) {
+      try {
+        return dynamicColorFromProto(
+            DynamicProto.DynamicColor.parseFrom(
+                byteArray, ExtensionRegistryLite.getEmptyRegistry()));
+      } catch (InvalidProtocolBufferException e) {
+        throw new IllegalArgumentException("Byte array could not be parsed into DynamicColor", e);
+      }
+    }
+
+    /** Creates a byte array that can later be used with {@link #fromByteArray(byte[])}. */
+    @NonNull
+    default byte[] toDynamicColorByteArray() {
+      return toDynamicColorProto().toByteArray();
+    }
+
     /** Creates a constant-valued {@link DynamicColor}. */
     @NonNull
     static DynamicColor constant(@ColorInt int constant) {
@@ -2812,7 +2924,8 @@
     @Nullable
     Fingerprint getFingerprint();
 
-    /** Builder to create {@link DynamicColor} objects.
+    /**
+     * Builder to create {@link DynamicColor} objects.
      *
      * @hide
      */
diff --git a/wear/protolayout/protolayout-expression/src/main/java/androidx/wear/protolayout/expression/FixedValueBuilders.java b/wear/protolayout/protolayout-expression/src/main/java/androidx/wear/protolayout/expression/FixedValueBuilders.java
index a1c4fa5..734e0d1 100644
--- a/wear/protolayout/protolayout-expression/src/main/java/androidx/wear/protolayout/expression/FixedValueBuilders.java
+++ b/wear/protolayout/protolayout-expression/src/main/java/androidx/wear/protolayout/expression/FixedValueBuilders.java
@@ -26,8 +26,9 @@
 import androidx.wear.protolayout.expression.proto.FixedProto;
 import androidx.wear.protolayout.expression.proto.StateEntryProto;
 
-/** Builders for fixed value primitive types that can be used in dynamic expressions and in for
- * state state valuess
+/**
+ * Builders for fixed value primitive types that can be used in dynamic expressions and in for state
+ * state values.
  */
 final class FixedValueBuilders {
   private FixedValueBuilders() {}
diff --git a/wear/protolayout/protolayout-expression/src/test/java/androidx/wear/protolayout/expression/DynamicBoolTest.java b/wear/protolayout/protolayout-expression/src/test/java/androidx/wear/protolayout/expression/DynamicBoolTest.java
index 073aa0f..d8ba4ed 100644
--- a/wear/protolayout/protolayout-expression/src/test/java/androidx/wear/protolayout/expression/DynamicBoolTest.java
+++ b/wear/protolayout/protolayout-expression/src/test/java/androidx/wear/protolayout/expression/DynamicBoolTest.java
@@ -18,68 +18,81 @@
 
 import static androidx.wear.protolayout.expression.proto.DynamicProto.LogicalOpType.LOGICAL_OP_TYPE_AND;
 import static androidx.wear.protolayout.expression.proto.DynamicProto.LogicalOpType.LOGICAL_OP_TYPE_OR;
-
 import static com.google.common.truth.Truth.assertThat;
+import static org.junit.Assert.assertThrows;
 
+import androidx.wear.protolayout.expression.DynamicBuilders.DynamicBool;
 import org.junit.Test;
 import org.junit.runner.RunWith;
 import org.robolectric.RobolectricTestRunner;
 
-import androidx.wear.protolayout.expression.DynamicBuilders.DynamicBool;
-
 @RunWith(RobolectricTestRunner.class)
 public final class DynamicBoolTest {
-    private static final String STATE_KEY = "state-key";
+  private static final String STATE_KEY = "state-key";
 
-    @Test
-    public void constantBool() {
-        DynamicBool falseBool = DynamicBool.constant(false);
-        DynamicBool trueBool = DynamicBool.constant(true);
+  @Test
+  public void constantBool() {
+    DynamicBool falseBool = DynamicBool.constant(false);
+    DynamicBool trueBool = DynamicBool.constant(true);
 
-        assertThat(falseBool.toDynamicBoolProto().getFixed().getValue()).isFalse();
-        assertThat(trueBool.toDynamicBoolProto().getFixed().getValue()).isTrue();
-    }
+    assertThat(falseBool.toDynamicBoolProto().getFixed().getValue()).isFalse();
+    assertThat(trueBool.toDynamicBoolProto().getFixed().getValue()).isTrue();
+  }
 
-    public void stateEntryValueBool() {
-        DynamicBool stateBool = DynamicBool.fromState(STATE_KEY);
+  @Test
+  public void stateEntryValueBool() {
+    DynamicBool stateBool = DynamicBool.fromState(STATE_KEY);
 
-        assertThat(stateBool.toDynamicBoolProto().getStateSource().getSourceKey()).isEqualTo(
-                STATE_KEY);
-    }
+    assertThat(stateBool.toDynamicBoolProto().getStateSource().getSourceKey()).isEqualTo(STATE_KEY);
+  }
 
-    @Test
-    public void andOpBool() {
-        DynamicBool firstBool = DynamicBool.constant(false);
-        DynamicBool secondBool = DynamicBool.constant(true);
+  @Test
+  public void andOpBool() {
+    DynamicBool firstBool = DynamicBool.constant(false);
+    DynamicBool secondBool = DynamicBool.constant(true);
 
-        DynamicBool result = firstBool.and(secondBool);
-        assertThat(result.toDynamicBoolProto().getLogicalOp().getOperationType())
-                .isEqualTo(LOGICAL_OP_TYPE_AND);
-        assertThat(result.toDynamicBoolProto().getLogicalOp().getInputLhs())
-                .isEqualTo(firstBool.toDynamicBoolProto());
-        assertThat(result.toDynamicBoolProto().getLogicalOp().getInputRhs())
-                .isEqualTo(secondBool.toDynamicBoolProto());
-    }
+    DynamicBool result = firstBool.and(secondBool);
+    assertThat(result.toDynamicBoolProto().getLogicalOp().getOperationType())
+        .isEqualTo(LOGICAL_OP_TYPE_AND);
+    assertThat(result.toDynamicBoolProto().getLogicalOp().getInputLhs())
+        .isEqualTo(firstBool.toDynamicBoolProto());
+    assertThat(result.toDynamicBoolProto().getLogicalOp().getInputRhs())
+        .isEqualTo(secondBool.toDynamicBoolProto());
+  }
 
-    @Test
-    public void orOpBool() {
-        DynamicBool firstBool = DynamicBool.constant(false);
-        DynamicBool secondBool = DynamicBool.constant(true);
+  @Test
+  public void orOpBool() {
+    DynamicBool firstBool = DynamicBool.constant(false);
+    DynamicBool secondBool = DynamicBool.constant(true);
 
-        DynamicBool result = firstBool.or(secondBool);
-        assertThat(result.toDynamicBoolProto().getLogicalOp().getOperationType())
-                .isEqualTo(LOGICAL_OP_TYPE_OR);
-        assertThat(result.toDynamicBoolProto().getLogicalOp().getInputLhs())
-                .isEqualTo(firstBool.toDynamicBoolProto());
-        assertThat(result.toDynamicBoolProto().getLogicalOp().getInputRhs())
-                .isEqualTo(secondBool.toDynamicBoolProto());
-    }
+    DynamicBool result = firstBool.or(secondBool);
+    assertThat(result.toDynamicBoolProto().getLogicalOp().getOperationType())
+        .isEqualTo(LOGICAL_OP_TYPE_OR);
+    assertThat(result.toDynamicBoolProto().getLogicalOp().getInputLhs())
+        .isEqualTo(firstBool.toDynamicBoolProto());
+    assertThat(result.toDynamicBoolProto().getLogicalOp().getInputRhs())
+        .isEqualTo(secondBool.toDynamicBoolProto());
+  }
 
-    public void negateOpBool() {
-        DynamicBool firstBool = DynamicBool.constant(true);
+  @Test
+  public void negateOpBool() {
+    DynamicBool firstBool = DynamicBool.constant(true);
 
-        assertThat(firstBool.isTrue().toDynamicBoolProto()).isEqualTo(firstBool);
-        assertThat(firstBool.toDynamicBoolProto().getNotOp().getInput())
-                .isEqualTo(firstBool);
-    }
+    assertThat(firstBool.isTrue().toDynamicBoolProto()).isEqualTo(firstBool.toDynamicBoolProto());
+    assertThat(firstBool.isFalse().toDynamicBoolProto().getNotOp().getInput())
+        .isEqualTo(firstBool.toDynamicBoolProto());
+  }
+
+  @Test
+  public void validProto() {
+    DynamicBool from = DynamicBool.constant(true);
+    DynamicBool to = DynamicBool.fromByteArray(from.toDynamicBoolByteArray());
+
+    assertThat(to.toDynamicBoolProto().getFixed().getValue()).isTrue();
+  }
+
+  @Test
+  public void invalidProto() {
+    assertThrows(IllegalArgumentException.class, () -> DynamicBool.fromByteArray(new byte[] {1}));
+  }
 }
diff --git a/wear/protolayout/protolayout-expression/src/test/java/androidx/wear/protolayout/expression/DynamicColorTest.java b/wear/protolayout/protolayout-expression/src/test/java/androidx/wear/protolayout/expression/DynamicColorTest.java
index aa4887a..3db0f1d 100644
--- a/wear/protolayout/protolayout-expression/src/test/java/androidx/wear/protolayout/expression/DynamicColorTest.java
+++ b/wear/protolayout/protolayout-expression/src/test/java/androidx/wear/protolayout/expression/DynamicColorTest.java
@@ -17,72 +17,87 @@
 package androidx.wear.protolayout.expression;
 
 import static androidx.wear.protolayout.expression.AnimationParameterBuilders.REPEAT_MODE_REVERSE;
-
 import static com.google.common.truth.Truth.assertThat;
+import static org.junit.Assert.assertThrows;
 
 import androidx.annotation.ColorInt;
-import androidx.wear.protolayout.expression.DynamicBuilders.DynamicColor;
 import androidx.wear.protolayout.expression.AnimationParameterBuilders.AnimationSpec;
 import androidx.wear.protolayout.expression.AnimationParameterBuilders.Repeatable;
-
+import androidx.wear.protolayout.expression.DynamicBuilders.DynamicColor;
 import org.junit.Test;
 import org.junit.runner.RunWith;
 import org.robolectric.RobolectricTestRunner;
 
 @RunWith(RobolectricTestRunner.class)
 public final class DynamicColorTest {
-    private static final String STATE_KEY = "state-key";
-    private static final @ColorInt int CONSTANT_VALUE = 0xff00ff00;
-    private static final AnimationSpec SPEC = new AnimationSpec.Builder().setDelayMillis(1)
-            .setDurationMillis(2).setRepeatable(new Repeatable.Builder()
-                    .setRepeatMode(REPEAT_MODE_REVERSE).setIterations(10).build()).build();
+  private static final String STATE_KEY = "state-key";
+  @ColorInt private static final int CONSTANT_VALUE = 0xff00ff00;
+  private static final AnimationSpec SPEC =
+      new AnimationSpec.Builder()
+          .setDelayMillis(1)
+          .setDurationMillis(2)
+          .setRepeatable(
+              new Repeatable.Builder().setRepeatMode(REPEAT_MODE_REVERSE).setIterations(10).build())
+          .build();
 
-    @Test
-    public void constantColor() {
-        DynamicColor constantColor = DynamicColor.constant(CONSTANT_VALUE);
+  @Test
+  public void constantColor() {
+    DynamicColor constantColor = DynamicColor.constant(CONSTANT_VALUE);
 
-        assertThat(constantColor.toDynamicColorProto().getFixed().getArgb())
-                .isEqualTo(CONSTANT_VALUE);
-    }
+    assertThat(constantColor.toDynamicColorProto().getFixed().getArgb()).isEqualTo(CONSTANT_VALUE);
+  }
 
-    @Test
-    public void stateEntryValueColor() {
-        DynamicColor stateColor = DynamicColor.fromState(STATE_KEY);
+  @Test
+  public void stateEntryValueColor() {
+    DynamicColor stateColor = DynamicColor.fromState(STATE_KEY);
 
-        assertThat(stateColor.toDynamicColorProto().getStateSource().getSourceKey()).isEqualTo(
-                STATE_KEY);
-    }
+    assertThat(stateColor.toDynamicColorProto().getStateSource().getSourceKey())
+        .isEqualTo(STATE_KEY);
+  }
 
-    @Test
-    public void rangeAnimatedColor() {
-        int startColor = 0xff00ff00;
-        int endColor = 0xff00ffff;
+  @Test
+  public void rangeAnimatedColor() {
+    int startColor = 0xff00ff00;
+    int endColor = 0xff00ffff;
 
-        DynamicColor animatedColor = DynamicColor.animate(startColor, endColor);
-        DynamicColor animatedColorWithSpec = DynamicColor.animate(startColor, endColor, SPEC);
+    DynamicColor animatedColor = DynamicColor.animate(startColor, endColor);
+    DynamicColor animatedColorWithSpec = DynamicColor.animate(startColor, endColor, SPEC);
 
-        assertThat(animatedColor.toDynamicColorProto().getAnimatableFixed().hasSpec()).isFalse();
-        assertThat(animatedColorWithSpec.toDynamicColorProto().getAnimatableFixed().getFromArgb())
-                .isEqualTo(startColor);
-        assertThat(animatedColorWithSpec.toDynamicColorProto().getAnimatableFixed().getToArgb())
-                .isEqualTo(endColor);
-        assertThat(animatedColorWithSpec.toDynamicColorProto().getAnimatableFixed().getSpec())
-                .isEqualTo(SPEC.toProto());
-    }
+    assertThat(animatedColor.toDynamicColorProto().getAnimatableFixed().hasSpec()).isFalse();
+    assertThat(animatedColorWithSpec.toDynamicColorProto().getAnimatableFixed().getFromArgb())
+        .isEqualTo(startColor);
+    assertThat(animatedColorWithSpec.toDynamicColorProto().getAnimatableFixed().getToArgb())
+        .isEqualTo(endColor);
+    assertThat(animatedColorWithSpec.toDynamicColorProto().getAnimatableFixed().getSpec())
+        .isEqualTo(SPEC.toProto());
+  }
 
-    @Test
-    public void stateAnimatedColor() {
-        DynamicColor stateColor = DynamicColor.fromState(STATE_KEY);
+  @Test
+  public void stateAnimatedColor() {
+    DynamicColor stateColor = DynamicColor.fromState(STATE_KEY);
 
-        DynamicColor animatedColor = DynamicColor.animate(STATE_KEY);
-        DynamicColor animatedColorWithSpec = DynamicColor.animate(STATE_KEY, SPEC);
+    DynamicColor animatedColor = DynamicColor.animate(STATE_KEY);
+    DynamicColor animatedColorWithSpec = DynamicColor.animate(STATE_KEY, SPEC);
 
-        assertThat(animatedColor.toDynamicColorProto().getAnimatableDynamic().hasSpec()).isFalse();
-        assertThat(animatedColorWithSpec.toDynamicColorProto().getAnimatableDynamic().getInput())
-                .isEqualTo(stateColor.toDynamicColorProto());
-        assertThat(animatedColorWithSpec.toDynamicColorProto().getAnimatableDynamic().getSpec())
-                .isEqualTo(SPEC.toProto());
-        assertThat(animatedColor.toDynamicColorProto())
-                .isEqualTo(stateColor.animate().toDynamicColorProto());
-    }
+    assertThat(animatedColor.toDynamicColorProto().getAnimatableDynamic().hasSpec()).isFalse();
+    assertThat(animatedColorWithSpec.toDynamicColorProto().getAnimatableDynamic().getInput())
+        .isEqualTo(stateColor.toDynamicColorProto());
+    assertThat(animatedColorWithSpec.toDynamicColorProto().getAnimatableDynamic().getSpec())
+        .isEqualTo(SPEC.toProto());
+    assertThat(animatedColor.toDynamicColorProto())
+        .isEqualTo(stateColor.animate().toDynamicColorProto());
+  }
+
+  @Test
+  public void validProto() {
+    DynamicColor from = DynamicColor.constant(CONSTANT_VALUE);
+    DynamicColor to = DynamicColor.fromByteArray(from.toDynamicColorByteArray());
+
+    assertThat(to.toDynamicColorProto().getFixed().getArgb()).isEqualTo(CONSTANT_VALUE);
+  }
+
+  @Test
+  public void invalidProto() {
+    assertThrows(IllegalArgumentException.class, () -> DynamicColor.fromByteArray(new byte[] {1}));
+  }
 }
diff --git a/wear/protolayout/protolayout-expression/src/test/java/androidx/wear/protolayout/expression/DynamicFloatTest.java b/wear/protolayout/protolayout-expression/src/test/java/androidx/wear/protolayout/expression/DynamicFloatTest.java
index c5f583a..9fb70d4 100644
--- a/wear/protolayout/protolayout-expression/src/test/java/androidx/wear/protolayout/expression/DynamicFloatTest.java
+++ b/wear/protolayout/protolayout-expression/src/test/java/androidx/wear/protolayout/expression/DynamicFloatTest.java
@@ -17,121 +17,144 @@
 package androidx.wear.protolayout.expression;
 
 import static androidx.wear.protolayout.expression.AnimationParameterBuilders.REPEAT_MODE_REVERSE;
-
 import static com.google.common.truth.Truth.assertThat;
+import static org.junit.Assert.assertThrows;
 
+import androidx.wear.protolayout.expression.AnimationParameterBuilders.AnimationSpec;
 import androidx.wear.protolayout.expression.DynamicBuilders.DynamicFloat;
 import androidx.wear.protolayout.expression.DynamicBuilders.DynamicInt32;
 import androidx.wear.protolayout.expression.DynamicBuilders.DynamicString;
 import androidx.wear.protolayout.expression.proto.DynamicProto;
-
 import org.junit.Test;
 import org.junit.runner.RunWith;
 import org.robolectric.RobolectricTestRunner;
 
 @RunWith(RobolectricTestRunner.class)
 public final class DynamicFloatTest {
-    private static final String STATE_KEY = "state-key";
-    private static final float CONSTANT_VALUE = 42.42f;
-    private static final AnimationParameterBuilders.AnimationSpec
-            SPEC = new AnimationParameterBuilders.AnimationSpec.Builder().setDelayMillis(1)
-            .setDurationMillis(2).setRepeatable(new AnimationParameterBuilders.Repeatable.Builder()
-                    .setRepeatMode(REPEAT_MODE_REVERSE).setIterations(10).build()).build();
+  private static final String STATE_KEY = "state-key";
+  private static final float CONSTANT_VALUE = 42.42f;
+  private static final AnimationSpec SPEC =
+      new AnimationSpec.Builder()
+          .setDelayMillis(1)
+          .setDurationMillis(2)
+          .setRepeatable(
+              new AnimationParameterBuilders.Repeatable.Builder()
+                  .setRepeatMode(REPEAT_MODE_REVERSE)
+                  .setIterations(10)
+                  .build())
+          .build();
 
-    @Test
-    public void constantFloat() {
-        DynamicFloat constantFloat = DynamicFloat.constant(CONSTANT_VALUE);
+  @Test
+  public void constantFloat() {
+    DynamicFloat constantFloat = DynamicFloat.constant(CONSTANT_VALUE);
 
-        assertThat(constantFloat.toDynamicFloatProto().getFixed().getValue())
-                .isWithin(0.0001f).of(CONSTANT_VALUE);
-    }
+    assertThat(constantFloat.toDynamicFloatProto().getFixed().getValue())
+        .isWithin(0.0001f)
+        .of(CONSTANT_VALUE);
+  }
 
-    @Test
-    public void stateEntryValueFloat() {
-        DynamicFloat stateFloat = DynamicFloat.fromState(STATE_KEY);
+  @Test
+  public void stateEntryValueFloat() {
+    DynamicFloat stateFloat = DynamicFloat.fromState(STATE_KEY);
 
-        assertThat(stateFloat.toDynamicFloatProto().getStateSource().getSourceKey()).isEqualTo(
-                STATE_KEY);
-    }
+    assertThat(stateFloat.toDynamicFloatProto().getStateSource().getSourceKey())
+        .isEqualTo(STATE_KEY);
+  }
 
-    @Test
-    public void constantFloat_asInt() {
-        DynamicFloat constantFloat = DynamicFloat.constant(CONSTANT_VALUE);
+  @Test
+  public void constantFloat_asInt() {
+    DynamicFloat constantFloat = DynamicFloat.constant(CONSTANT_VALUE);
 
-        DynamicInt32 dynamicInt32 = constantFloat.asInt();
+    DynamicInt32 dynamicInt32 = constantFloat.asInt();
 
-        assertThat(dynamicInt32.toDynamicInt32Proto().getFloatToInt()
-                .getInput().getFixed().getValue()).isWithin(0.0001f).of(CONSTANT_VALUE);
-    }
+    assertThat(dynamicInt32.toDynamicInt32Proto().getFloatToInt().getInput().getFixed().getValue())
+        .isWithin(0.0001f)
+        .of(CONSTANT_VALUE);
+  }
 
-    @Test
-    public void formatFloat_defaultParameters() {
-        DynamicFloat constantFloat = DynamicFloat.constant(CONSTANT_VALUE);
+  @Test
+  public void formatFloat_defaultParameters() {
+    DynamicFloat constantFloat = DynamicFloat.constant(CONSTANT_VALUE);
 
-        DynamicString defaultFormat = constantFloat.format();
+    DynamicString defaultFormat = constantFloat.format();
 
-        DynamicProto.FloatFormatOp floatFormatOp =
-                defaultFormat.toDynamicStringProto().getFloatFormatOp();
-        assertThat(floatFormatOp.getInput()).isEqualTo(constantFloat.toDynamicFloatProto());
-        assertThat(floatFormatOp.getGroupingUsed()).isEqualTo(false);
-        assertThat(floatFormatOp.hasMaxFractionDigits()).isFalse();
-        assertThat(floatFormatOp.getMinFractionDigits()).isEqualTo(0);
-        assertThat(floatFormatOp.hasMinIntegerDigits()).isFalse();
-    }
-    @Test
-    public void formatFloat_customFormatter() {
-        DynamicFloat constantFloat = DynamicFloat.constant(CONSTANT_VALUE);
-        boolean groupingUsed = true;
-        int minFractionDigits = 1;
-        int maxFractionDigits = 2;
-        int minIntegerDigits = 3;
-        DynamicFloat.FloatFormatter floatFormatter =
-                DynamicFloat.FloatFormatter.with().minFractionDigits(minFractionDigits)
-                        .maxFractionDigits(maxFractionDigits).minIntegerDigits(minIntegerDigits)
-                        .groupingUsed(groupingUsed);
+    DynamicProto.FloatFormatOp floatFormatOp =
+        defaultFormat.toDynamicStringProto().getFloatFormatOp();
+    assertThat(floatFormatOp.getInput()).isEqualTo(constantFloat.toDynamicFloatProto());
+    assertThat(floatFormatOp.getGroupingUsed()).isFalse();
+    assertThat(floatFormatOp.hasMaxFractionDigits()).isFalse();
+    assertThat(floatFormatOp.getMinFractionDigits()).isEqualTo(0);
+    assertThat(floatFormatOp.hasMinIntegerDigits()).isFalse();
+  }
 
-        DynamicString customFormat = constantFloat.format(floatFormatter);
+  @Test
+  public void formatFloat_customFormatter() {
+    DynamicFloat constantFloat = DynamicFloat.constant(CONSTANT_VALUE);
+    boolean groupingUsed = true;
+    int minFractionDigits = 1;
+    int maxFractionDigits = 2;
+    int minIntegerDigits = 3;
+    DynamicFloat.FloatFormatter floatFormatter =
+        DynamicFloat.FloatFormatter.with()
+            .minFractionDigits(minFractionDigits)
+            .maxFractionDigits(maxFractionDigits)
+            .minIntegerDigits(minIntegerDigits)
+            .groupingUsed(groupingUsed);
 
-        DynamicProto.FloatFormatOp floatFormatOp =
-                customFormat.toDynamicStringProto().getFloatFormatOp();
-        assertThat(floatFormatOp.getInput()).isEqualTo(constantFloat.toDynamicFloatProto());
-        assertThat(floatFormatOp.getGroupingUsed()).isEqualTo(groupingUsed);
-        assertThat(floatFormatOp.getMaxFractionDigits()).isEqualTo(maxFractionDigits);
-        assertThat(floatFormatOp.getMinFractionDigits()).isEqualTo(minFractionDigits);
-        assertThat(floatFormatOp.getMinIntegerDigits()).isEqualTo(minIntegerDigits);
-    }
+    DynamicString customFormat = constantFloat.format(floatFormatter);
 
-    @Test
-    public void rangeAnimatedFloat() {
-        float startFloat = 100f;
-        float endFloat = 200f;
+    DynamicProto.FloatFormatOp floatFormatOp =
+        customFormat.toDynamicStringProto().getFloatFormatOp();
+    assertThat(floatFormatOp.getInput()).isEqualTo(constantFloat.toDynamicFloatProto());
+    assertThat(floatFormatOp.getGroupingUsed()).isEqualTo(groupingUsed);
+    assertThat(floatFormatOp.getMaxFractionDigits()).isEqualTo(maxFractionDigits);
+    assertThat(floatFormatOp.getMinFractionDigits()).isEqualTo(minFractionDigits);
+    assertThat(floatFormatOp.getMinIntegerDigits()).isEqualTo(minIntegerDigits);
+  }
 
-        DynamicFloat animatedFloat = DynamicFloat.animate(startFloat,
-                endFloat);
-        DynamicFloat animatedFloatWithSpec = DynamicFloat.animate(startFloat, endFloat, SPEC);
+  @Test
+  public void rangeAnimatedFloat() {
+    float startFloat = 100f;
+    float endFloat = 200f;
 
-        assertThat(animatedFloat.toDynamicFloatProto().getAnimatableFixed().hasSpec()).isFalse();
-        assertThat(animatedFloatWithSpec.toDynamicFloatProto().getAnimatableFixed().getFromValue())
-                .isEqualTo(startFloat);
-        assertThat(animatedFloatWithSpec.toDynamicFloatProto().getAnimatableFixed().getToValue())
-                .isEqualTo(endFloat);
-        assertThat(animatedFloatWithSpec.toDynamicFloatProto().getAnimatableFixed().getSpec())
-                .isEqualTo(SPEC.toProto());
-    }
+    DynamicFloat animatedFloat = DynamicFloat.animate(startFloat, endFloat);
+    DynamicFloat animatedFloatWithSpec = DynamicFloat.animate(startFloat, endFloat, SPEC);
 
-    @Test
-    public void stateAnimatedFloat() {
-        DynamicFloat stateFloat = DynamicFloat.fromState(STATE_KEY);
+    assertThat(animatedFloat.toDynamicFloatProto().getAnimatableFixed().hasSpec()).isFalse();
+    assertThat(animatedFloatWithSpec.toDynamicFloatProto().getAnimatableFixed().getFromValue())
+        .isEqualTo(startFloat);
+    assertThat(animatedFloatWithSpec.toDynamicFloatProto().getAnimatableFixed().getToValue())
+        .isEqualTo(endFloat);
+    assertThat(animatedFloatWithSpec.toDynamicFloatProto().getAnimatableFixed().getSpec())
+        .isEqualTo(SPEC.toProto());
+  }
 
-        DynamicFloat animatedColor = DynamicFloat.animate(STATE_KEY);
-        DynamicFloat animatedColorWithSpec = DynamicFloat.animate(STATE_KEY, SPEC);
+  @Test
+  public void stateAnimatedFloat() {
+    DynamicFloat stateFloat = DynamicFloat.fromState(STATE_KEY);
 
-        assertThat(animatedColor.toDynamicFloatProto().getAnimatableDynamic().hasSpec()).isFalse();
-        assertThat(animatedColorWithSpec.toDynamicFloatProto().getAnimatableDynamic().getInput())
-                .isEqualTo(stateFloat.toDynamicFloatProto());
-        assertThat(animatedColorWithSpec.toDynamicFloatProto().getAnimatableDynamic().getSpec())
-                .isEqualTo(SPEC.toProto());
-        assertThat(animatedColor.toDynamicFloatProto())
-                .isEqualTo(stateFloat.animate().toDynamicFloatProto());
-    }
+    DynamicFloat animatedFloat = DynamicFloat.animate(STATE_KEY);
+    DynamicFloat animatedFloatWithSpec = DynamicFloat.animate(STATE_KEY, SPEC);
+
+    assertThat(animatedFloat.toDynamicFloatProto().getAnimatableDynamic().hasSpec()).isFalse();
+    assertThat(animatedFloatWithSpec.toDynamicFloatProto().getAnimatableDynamic().getInput())
+        .isEqualTo(stateFloat.toDynamicFloatProto());
+    assertThat(animatedFloatWithSpec.toDynamicFloatProto().getAnimatableDynamic().getSpec())
+        .isEqualTo(SPEC.toProto());
+    assertThat(animatedFloat.toDynamicFloatProto())
+        .isEqualTo(stateFloat.animate().toDynamicFloatProto());
+  }
+
+  @Test
+  public void validProto() {
+    DynamicFloat from = DynamicFloat.constant(CONSTANT_VALUE);
+    DynamicFloat to = DynamicFloat.fromByteArray(from.toDynamicFloatByteArray());
+
+    assertThat(to.toDynamicFloatProto().getFixed().getValue()).isEqualTo(CONSTANT_VALUE);
+  }
+
+  @Test
+  public void invalidProto() {
+    assertThrows(IllegalArgumentException.class, () -> DynamicFloat.fromByteArray(new byte[] {1}));
+  }
 }
diff --git a/wear/protolayout/protolayout-expression/src/test/java/androidx/wear/protolayout/expression/DynamicInt32Test.java b/wear/protolayout/protolayout-expression/src/test/java/androidx/wear/protolayout/expression/DynamicInt32Test.java
index aa13b08..0c9ece1 100644
--- a/wear/protolayout/protolayout-expression/src/test/java/androidx/wear/protolayout/expression/DynamicInt32Test.java
+++ b/wear/protolayout/protolayout-expression/src/test/java/androidx/wear/protolayout/expression/DynamicInt32Test.java
@@ -17,71 +17,93 @@
 package androidx.wear.protolayout.expression;
 
 import static com.google.common.truth.Truth.assertThat;
+import static org.junit.Assert.assertThrows;
 
+import androidx.wear.protolayout.expression.DynamicBuilders.DynamicFloat;
+import androidx.wear.protolayout.expression.DynamicBuilders.DynamicInt32;
+import androidx.wear.protolayout.expression.proto.DynamicProto;
 import org.junit.Test;
 import org.junit.runner.RunWith;
 import org.robolectric.RobolectricTestRunner;
 
-import androidx.wear.protolayout.expression.DynamicBuilders.DynamicInt32;
-import androidx.wear.protolayout.expression.DynamicBuilders.DynamicFloat;
-import androidx.wear.protolayout.expression.proto.DynamicProto;
-
 @RunWith(RobolectricTestRunner.class)
 public final class DynamicInt32Test {
-    private static final String STATE_KEY = "state-key";
-    private static final int CONSTANT_VALUE = 42;
+  private static final String STATE_KEY = "state-key";
+  private static final int CONSTANT_VALUE = 42;
 
-    @Test
-    public void constantInt32() {
-        DynamicInt32 constantInt32 = DynamicInt32.constant(CONSTANT_VALUE);
+  @Test
+  public void constantInt32() {
+    DynamicInt32 constantInt32 = DynamicInt32.constant(CONSTANT_VALUE);
 
-        assertThat(constantInt32.toDynamicInt32Proto().getFixed().getValue())
-                .isEqualTo(CONSTANT_VALUE);
-    }
+    assertThat(constantInt32.toDynamicInt32Proto().getFixed().getValue()).isEqualTo(CONSTANT_VALUE);
+  }
 
-    @Test
-    public void stateEntryValueInt32() {
-        DynamicInt32 stateInt32 = DynamicInt32.fromState(STATE_KEY);
+  @Test
+  public void stateEntryValueInt32() {
+    DynamicInt32 stateInt32 = DynamicInt32.fromState(STATE_KEY);
 
-        assertThat(stateInt32.toDynamicInt32Proto().getStateSource().getSourceKey()).isEqualTo(
-                STATE_KEY);
-    }
+    assertThat(stateInt32.toDynamicInt32Proto().getStateSource().getSourceKey())
+        .isEqualTo(STATE_KEY);
+  }
 
-    @Test
-    public void constantInt32_asFloat() {
-        DynamicInt32 constantInt32 = DynamicInt32.constant(CONSTANT_VALUE);
+  @Test
+  public void constantInt32_asFloat() {
+    DynamicInt32 constantInt32 = DynamicInt32.constant(CONSTANT_VALUE);
 
-        DynamicFloat dynamicFloat = constantInt32.asFloat();
+    DynamicFloat dynamicFloat = constantInt32.asFloat();
 
-        assertThat(dynamicFloat.toDynamicFloatProto().getInt32ToFloatOperation()
-                .getInput().getFixed().getValue()).isEqualTo(CONSTANT_VALUE);
-    }
+    assertThat(
+            dynamicFloat
+                .toDynamicFloatProto()
+                .getInt32ToFloatOperation()
+                .getInput()
+                .getFixed()
+                .getValue())
+        .isEqualTo(CONSTANT_VALUE);
+  }
 
-    public void formatInt32_defaultParameters() {
-        DynamicInt32 constantInt32 = DynamicInt32.constant(CONSTANT_VALUE);
+  @Test
+  public void formatInt32_defaultParameters() {
+    DynamicInt32 constantInt32 = DynamicInt32.constant(CONSTANT_VALUE);
 
-        DynamicBuilders.DynamicString defaultFormat = constantInt32.format();
+    DynamicBuilders.DynamicString defaultFormat = constantInt32.format();
 
-        DynamicProto.Int32FormatOp int32FormatOp =
-                defaultFormat.toDynamicStringProto().getInt32FormatOp();
-        assertThat(int32FormatOp.getInput()).isEqualTo(constantInt32.toDynamicInt32Proto());
-        assertThat(int32FormatOp.getGroupingUsed()).isEqualTo(false);
-        assertThat(int32FormatOp.hasMinIntegerDigits()).isFalse();
-    }
-    @Test
-    public void formatInt32_customFormatter() {
-        DynamicInt32 constantInt32 = DynamicInt32.constant(CONSTANT_VALUE);
-        boolean groupingUsed = true;
-        int minIntegerDigits = 3;
-        DynamicInt32.IntFormatter intFormatter = DynamicInt32.IntFormatter.with()
-                .minIntegerDigits(minIntegerDigits).groupingUsed(groupingUsed);
+    DynamicProto.Int32FormatOp int32FormatOp =
+        defaultFormat.toDynamicStringProto().getInt32FormatOp();
+    assertThat(int32FormatOp.getInput()).isEqualTo(constantInt32.toDynamicInt32Proto());
+    assertThat(int32FormatOp.getGroupingUsed()).isFalse();
+    assertThat(int32FormatOp.hasMinIntegerDigits()).isFalse();
+  }
 
-        DynamicBuilders.DynamicString customFormat = constantInt32.format(intFormatter);
+  @Test
+  public void formatInt32_customFormatter() {
+    DynamicInt32 constantInt32 = DynamicInt32.constant(CONSTANT_VALUE);
+    boolean groupingUsed = true;
+    int minIntegerDigits = 3;
+    DynamicInt32.IntFormatter intFormatter =
+        DynamicInt32.IntFormatter.with()
+            .minIntegerDigits(minIntegerDigits)
+            .groupingUsed(groupingUsed);
 
-        DynamicProto.Int32FormatOp int32FormatOp =
-                customFormat.toDynamicStringProto().getInt32FormatOp();
-        assertThat(int32FormatOp.getInput()).isEqualTo(constantInt32.toDynamicInt32Proto());
-        assertThat(int32FormatOp.getGroupingUsed()).isEqualTo(groupingUsed);
-        assertThat(int32FormatOp.getMinIntegerDigits()).isEqualTo(minIntegerDigits);
-    }
+    DynamicBuilders.DynamicString customFormat = constantInt32.format(intFormatter);
+
+    DynamicProto.Int32FormatOp int32FormatOp =
+        customFormat.toDynamicStringProto().getInt32FormatOp();
+    assertThat(int32FormatOp.getInput()).isEqualTo(constantInt32.toDynamicInt32Proto());
+    assertThat(int32FormatOp.getGroupingUsed()).isEqualTo(groupingUsed);
+    assertThat(int32FormatOp.getMinIntegerDigits()).isEqualTo(minIntegerDigits);
+  }
+
+  @Test
+  public void validProto() {
+    DynamicInt32 from = DynamicInt32.constant(CONSTANT_VALUE);
+    DynamicInt32 to = DynamicInt32.fromByteArray(from.toDynamicInt32ByteArray());
+
+    assertThat(to.toDynamicInt32Proto().getFixed().getValue()).isEqualTo(CONSTANT_VALUE);
+  }
+
+  @Test
+  public void invalidProto() {
+    assertThrows(IllegalArgumentException.class, () -> DynamicInt32.fromByteArray(new byte[] {1}));
+  }
 }
diff --git a/wear/protolayout/protolayout-expression/src/test/java/androidx/wear/protolayout/expression/DynamicStringTest.java b/wear/protolayout/protolayout-expression/src/test/java/androidx/wear/protolayout/expression/DynamicStringTest.java
index bca7fca..6eaca2d 100644
--- a/wear/protolayout/protolayout-expression/src/test/java/androidx/wear/protolayout/expression/DynamicStringTest.java
+++ b/wear/protolayout/protolayout-expression/src/test/java/androidx/wear/protolayout/expression/DynamicStringTest.java
@@ -17,76 +17,90 @@
 package androidx.wear.protolayout.expression;
 
 import static com.google.common.truth.Truth.assertThat;
+import static org.junit.Assert.assertThrows;
 
+import androidx.wear.protolayout.expression.DynamicBuilders.DynamicBool;
 import androidx.wear.protolayout.expression.DynamicBuilders.DynamicString;
 import androidx.wear.protolayout.expression.proto.DynamicProto;
-
 import org.junit.Test;
 import org.junit.runner.RunWith;
 import org.robolectric.RobolectricTestRunner;
 
 @RunWith(RobolectricTestRunner.class)
 public final class DynamicStringTest {
-    private static final String STATE_KEY = "state-key";
-    private static final String CONSTANT_VALUE = "constant-value";
+  private static final String STATE_KEY = "state-key";
+  private static final String CONSTANT_VALUE = "constant-value";
 
-    @Test
-    public void constantString() {
-        DynamicString constantString = DynamicString.constant(CONSTANT_VALUE);
+  @Test
+  public void constantString() {
+    DynamicString constantString = DynamicString.constant(CONSTANT_VALUE);
 
-        assertThat(constantString.toDynamicStringProto().getFixed().getValue())
-                .isEqualTo(CONSTANT_VALUE);
-    }
+    assertThat(constantString.toDynamicStringProto().getFixed().getValue())
+        .isEqualTo(CONSTANT_VALUE);
+  }
 
-    @Test
-    public void stateEntryValueString() {
-        DynamicString stateString = DynamicString.fromState(STATE_KEY);
+  @Test
+  public void stateEntryValueString() {
+    DynamicString stateString = DynamicString.fromState(STATE_KEY);
 
-        assertThat(stateString.toDynamicStringProto().getStateSource().getSourceKey()).isEqualTo(
-                STATE_KEY);
-    }
+    assertThat(stateString.toDynamicStringProto().getStateSource().getSourceKey())
+        .isEqualTo(STATE_KEY);
+  }
 
-    @Test
-    public void constantString_concat() {
-        DynamicString firstString = DynamicString.constant(CONSTANT_VALUE);
-        DynamicString secondString = DynamicString.constant(CONSTANT_VALUE);
+  @Test
+  public void constantString_concat() {
+    DynamicString firstString = DynamicString.constant(CONSTANT_VALUE);
+    DynamicString secondString = DynamicString.constant(CONSTANT_VALUE);
 
-        DynamicString resultString = firstString.concat(secondString);
+    DynamicString resultString = firstString.concat(secondString);
 
-        DynamicProto.ConcatStringOp concatOp = resultString.toDynamicStringProto().getConcatOp();
-        assertThat(concatOp.getInputLhs()).isEqualTo(firstString.toDynamicStringProto());
-        assertThat(concatOp.getInputRhs()).isEqualTo(secondString.toDynamicStringProto());
-    }
+    DynamicProto.ConcatStringOp concatOp = resultString.toDynamicStringProto().getConcatOp();
+    assertThat(concatOp.getInputLhs()).isEqualTo(firstString.toDynamicStringProto());
+    assertThat(concatOp.getInputRhs()).isEqualTo(secondString.toDynamicStringProto());
+  }
 
-    @Test
-    public void constantString_conditional() {
-        DynamicBuilders.DynamicBool condition = DynamicBuilders.DynamicBool.constant(true);
-        DynamicString firstString = DynamicString.constant(CONSTANT_VALUE);
-        DynamicString secondString = DynamicString.constant(CONSTANT_VALUE);
+  @Test
+  public void constantString_conditional() {
+    DynamicBool condition = DynamicBool.constant(true);
+    DynamicString firstString = DynamicString.constant(CONSTANT_VALUE);
+    DynamicString secondString = DynamicString.constant(CONSTANT_VALUE);
 
-        DynamicString resultString =
-                DynamicString.onCondition(condition).use(firstString).elseUse(secondString);
+    DynamicString resultString =
+        DynamicString.onCondition(condition).use(firstString).elseUse(secondString);
 
-        DynamicProto.ConditionalStringOp conditionalOp = resultString.toDynamicStringProto()
-                .getConditionalOp();
-        assertThat(conditionalOp.getCondition()).isEqualTo(condition.toDynamicBoolProto());
-        assertThat(conditionalOp.getValueIfTrue()).isEqualTo(firstString.toDynamicStringProto());
-        assertThat(conditionalOp.getValueIfFalse()).isEqualTo(secondString.toDynamicStringProto());
-    }
+    DynamicProto.ConditionalStringOp conditionalOp =
+        resultString.toDynamicStringProto().getConditionalOp();
+    assertThat(conditionalOp.getCondition()).isEqualTo(condition.toDynamicBoolProto());
+    assertThat(conditionalOp.getValueIfTrue()).isEqualTo(firstString.toDynamicStringProto());
+    assertThat(conditionalOp.getValueIfFalse()).isEqualTo(secondString.toDynamicStringProto());
+  }
 
-    @Test
-    public void rawString_conditional() {
-        DynamicBuilders.DynamicBool condition = DynamicBuilders.DynamicBool.constant(true);
-        String firstString = "raw-string";
-        DynamicString secondString = DynamicString.constant(CONSTANT_VALUE);
+  @Test
+  public void rawString_conditional() {
+    DynamicBool condition = DynamicBool.constant(true);
+    String firstString = "raw-string";
+    DynamicString secondString = DynamicString.constant(CONSTANT_VALUE);
 
-        DynamicString resultString =
-                DynamicString.onCondition(condition).use(firstString).elseUse(secondString);
+    DynamicString resultString =
+        DynamicString.onCondition(condition).use(firstString).elseUse(secondString);
 
-        DynamicProto.ConditionalStringOp conditionalOp = resultString.toDynamicStringProto()
-                .getConditionalOp();
-        assertThat(conditionalOp.getCondition()).isEqualTo(condition.toDynamicBoolProto());
-        assertThat(conditionalOp.getValueIfTrue().getFixed().getValue()).isEqualTo(firstString);
-        assertThat(conditionalOp.getValueIfFalse()).isEqualTo(secondString.toDynamicStringProto());
-    }
+    DynamicProto.ConditionalStringOp conditionalOp =
+        resultString.toDynamicStringProto().getConditionalOp();
+    assertThat(conditionalOp.getCondition()).isEqualTo(condition.toDynamicBoolProto());
+    assertThat(conditionalOp.getValueIfTrue().getFixed().getValue()).isEqualTo(firstString);
+    assertThat(conditionalOp.getValueIfFalse()).isEqualTo(secondString.toDynamicStringProto());
+  }
+
+  @Test
+  public void validProto() {
+    DynamicString from = DynamicString.constant(CONSTANT_VALUE);
+    DynamicString to = DynamicString.fromByteArray(from.toDynamicStringByteArray());
+
+    assertThat(to.toDynamicStringProto().getFixed().getValue()).isEqualTo(CONSTANT_VALUE);
+  }
+
+  @Test
+  public void invalidProto() {
+    assertThrows(IllegalArgumentException.class, () -> DynamicString.fromByteArray(new byte[] {1}));
+  }
 }
diff --git a/wear/protolayout/protolayout-expression/src/test/java/androidx/wear/protolayout/expression/FingerprintTest.java b/wear/protolayout/protolayout-expression/src/test/java/androidx/wear/protolayout/expression/FingerprintTest.java
index 78ae84f..c090cf0 100644
--- a/wear/protolayout/protolayout-expression/src/test/java/androidx/wear/protolayout/expression/FingerprintTest.java
+++ b/wear/protolayout/protolayout-expression/src/test/java/androidx/wear/protolayout/expression/FingerprintTest.java
@@ -24,48 +24,48 @@
 
 @RunWith(RobolectricTestRunner.class)
 public final class FingerprintTest {
-    private final static int SELF_TYPE_VALUE = 1234;
-    private final static int FIELD_1 = 1;
-    private final static int VALUE_HASH1 = 10;
+  private static final int SELF_TYPE_VALUE = 1234;
+  private static final int FIELD_1 = 1;
+  private static final int VALUE_HASH1 = 10;
 
-    private final static int DISCARDED_VALUE = -1;
+  private static final int DISCARDED_VALUE = -1;
 
-    @Test
-    public void addChildNode() {
-        Fingerprint parentFingerPrint = new Fingerprint(SELF_TYPE_VALUE);
-        Fingerprint childFingerPrint = new Fingerprint(SELF_TYPE_VALUE);
-        childFingerPrint.recordPropertyUpdate(FIELD_1, VALUE_HASH1);
+  @Test
+  public void addChildNode() {
+    Fingerprint parentFingerPrint = new Fingerprint(SELF_TYPE_VALUE);
+    Fingerprint childFingerPrint = new Fingerprint(SELF_TYPE_VALUE);
+    childFingerPrint.recordPropertyUpdate(FIELD_1, VALUE_HASH1);
 
-        parentFingerPrint.addChildNode(childFingerPrint);
+    parentFingerPrint.addChildNode(childFingerPrint);
 
-        assertThat(parentFingerPrint.childNodes()).containsExactly(childFingerPrint);
-    }
+    assertThat(parentFingerPrint.childNodes()).containsExactly(childFingerPrint);
+  }
 
-    @Test
-    public void discard_clearsSelfFingerprint() {
-        Fingerprint parentFingerPrint = new Fingerprint(SELF_TYPE_VALUE);
-        Fingerprint childFingerPrint = new Fingerprint(SELF_TYPE_VALUE);
-        childFingerPrint.recordPropertyUpdate(FIELD_1, VALUE_HASH1);
-        parentFingerPrint.addChildNode(childFingerPrint);
+  @Test
+  public void discard_clearsSelfFingerprint() {
+    Fingerprint parentFingerPrint = new Fingerprint(SELF_TYPE_VALUE);
+    Fingerprint childFingerPrint = new Fingerprint(SELF_TYPE_VALUE);
+    childFingerPrint.recordPropertyUpdate(FIELD_1, VALUE_HASH1);
+    parentFingerPrint.addChildNode(childFingerPrint);
 
-        parentFingerPrint.discardValues(/* includeChildren= */false);
+    parentFingerPrint.discardValues(/* includeChildren= */ false);
 
-        assertThat(parentFingerPrint.selfPropsValue()).isEqualTo(DISCARDED_VALUE);
-        assertThat(parentFingerPrint.childNodes()).containsExactly(childFingerPrint);
-        assertThat(parentFingerPrint.childNodesValue()).isNotEqualTo(DISCARDED_VALUE);
-    }
+    assertThat(parentFingerPrint.selfPropsValue()).isEqualTo(DISCARDED_VALUE);
+    assertThat(parentFingerPrint.childNodes()).containsExactly(childFingerPrint);
+    assertThat(parentFingerPrint.childNodesValue()).isNotEqualTo(DISCARDED_VALUE);
+  }
 
-    @Test
-    public void discard_includeChildren_clearsSelfAndChildrenFingerprint() {
-        Fingerprint parentFingerPrint = new Fingerprint(SELF_TYPE_VALUE);
-        Fingerprint childFingerPrint = new Fingerprint(SELF_TYPE_VALUE);
-        childFingerPrint.recordPropertyUpdate(FIELD_1, VALUE_HASH1);
-        parentFingerPrint.addChildNode(childFingerPrint);
+  @Test
+  public void discard_includeChildren_clearsSelfAndChildrenFingerprint() {
+    Fingerprint parentFingerPrint = new Fingerprint(SELF_TYPE_VALUE);
+    Fingerprint childFingerPrint = new Fingerprint(SELF_TYPE_VALUE);
+    childFingerPrint.recordPropertyUpdate(FIELD_1, VALUE_HASH1);
+    parentFingerPrint.addChildNode(childFingerPrint);
 
-        parentFingerPrint.discardValues(/* includeChildren= */true);
+    parentFingerPrint.discardValues(/* includeChildren= */ true);
 
-        assertThat(parentFingerPrint.selfPropsValue()).isEqualTo(DISCARDED_VALUE);
-        assertThat(parentFingerPrint.childNodes()).isEmpty();
-        assertThat(parentFingerPrint.childNodesValue()).isEqualTo(DISCARDED_VALUE);
-    }
+    assertThat(parentFingerPrint.selfPropsValue()).isEqualTo(DISCARDED_VALUE);
+    assertThat(parentFingerPrint.childNodes()).isEmpty();
+    assertThat(parentFingerPrint.childNodesValue()).isEqualTo(DISCARDED_VALUE);
+  }
 }
diff --git a/wear/protolayout/protolayout-expression/src/test/java/androidx/wear/protolayout/expression/StateEntryValueTest.java b/wear/protolayout/protolayout-expression/src/test/java/androidx/wear/protolayout/expression/StateEntryValueTest.java
index 0a70e13..fae643d 100644
--- a/wear/protolayout/protolayout-expression/src/test/java/androidx/wear/protolayout/expression/StateEntryValueTest.java
+++ b/wear/protolayout/protolayout-expression/src/test/java/androidx/wear/protolayout/expression/StateEntryValueTest.java
@@ -19,51 +19,48 @@
 import static com.google.common.truth.Truth.assertThat;
 
 import androidx.wear.protolayout.expression.StateEntryBuilders.StateEntryValue;
-
 import org.junit.Test;
 import org.junit.runner.RunWith;
 import org.robolectric.RobolectricTestRunner;
 
 @RunWith(RobolectricTestRunner.class)
 public final class StateEntryValueTest {
-    private static final String STATE_KEY = "state-key";
+  @Test
+  public void boolStateEntryValue() {
+    StateEntryValue boolStateEntryValue = StateEntryValue.fromBool(true);
 
-    @Test
-    public void boolStateEntryValue() {
-        StateEntryValue boolStateEntryValue = StateEntryValue.fromBool(true);
+    assertThat(boolStateEntryValue.toStateEntryValueProto().getBoolVal().getValue()).isTrue();
+  }
 
-        assertThat(boolStateEntryValue.toStateEntryValueProto().getBoolVal().getValue()).isTrue();
-    }
+  @Test
+  public void colorStateEntryValue() {
+    StateEntryValue colorStateEntryValue = StateEntryValue.fromColor(0xff00ff00);
 
-    @Test
-    public void colorStateEntryValue() {
-        StateEntryValue colorStateEntryValue = StateEntryValue.fromColor(0xff00ff00);
+    assertThat(colorStateEntryValue.toStateEntryValueProto().getColorVal().getArgb())
+        .isEqualTo(0xff00ff00);
+  }
 
-        assertThat(colorStateEntryValue.toStateEntryValueProto().getColorVal().getArgb())
-                .isEqualTo(0xff00ff00);
-    }
+  @Test
+  public void floatStateEntryValue() {
+    StateEntryValue floatStateEntryValue = StateEntryValue.fromFloat(42.42f);
 
-    @Test
-    public void floatStateEntryValue() {
-        StateEntryValue floatStateEntryValue = StateEntryValue.fromFloat(42.42f);
+    assertThat(floatStateEntryValue.toStateEntryValueProto().getFloatVal().getValue())
+        .isWithin(0.0001f)
+        .of(42.42f);
+  }
 
-        assertThat(floatStateEntryValue.toStateEntryValueProto().getFloatVal().getValue())
-                .isWithin(0.0001f).of(42.42f);
-    }
+  @Test
+  public void intStateEntryValue() {
+    StateEntryValue intStateEntryValue = StateEntryValue.fromInt(42);
 
-    @Test
-    public void intStateEntryValue() {
-        StateEntryValue intStateEntryValue = StateEntryValue.fromInt(42);
+    assertThat(intStateEntryValue.toStateEntryValueProto().getInt32Val().getValue()).isEqualTo(42);
+  }
 
-        assertThat(intStateEntryValue.toStateEntryValueProto().getInt32Val().getValue())
-                .isEqualTo(42);
-    }
+  @Test
+  public void stringStateEntryValue() {
+    StateEntryValue stringStateEntryValue = StateEntryValue.fromString("constant-value");
 
-    @Test
-    public void stringStateEntryValue() {
-        StateEntryValue stringStateEntryValue = StateEntryValue.fromString("constant-value");
-
-        assertThat(stringStateEntryValue.toStateEntryValueProto().getStringVal().getValue())
-                .isEqualTo("constant-value");
-    }
+    assertThat(stringStateEntryValue.toStateEntryValueProto().getStringVal().getValue())
+        .isEqualTo("constant-value");
+  }
 }
diff --git a/wear/protolayout/protolayout-proto/build.gradle b/wear/protolayout/protolayout-proto/build.gradle
index 00c8979..e48edfc 100644
--- a/wear/protolayout/protolayout-proto/build.gradle
+++ b/wear/protolayout/protolayout-proto/build.gradle
@@ -25,6 +25,10 @@
     shadowed
     compileOnly.extendsFrom(shadowed)
     testCompile.extendsFrom(shadowed)
+    // for downstream tests, provide a configuration that includes the shadow output + other
+    // dependencies that are not shadowed
+    shadowAndImplementation.extendsFrom(shadow)
+    shadowAndImplementation.extendsFrom(implementation)
 }
 dependencies {
     implementation("androidx.annotation:annotation:1.1.0")
diff --git a/wear/watchface/watchface-client/api/current.txt b/wear/watchface/watchface-client/api/current.txt
index 40bcc03..aba37f5 100644
--- a/wear/watchface/watchface-client/api/current.txt
+++ b/wear/watchface/watchface-client/api/current.txt
@@ -141,6 +141,7 @@
     method @kotlin.jvm.Throws(exceptionClasses=RemoteException::class) public default androidx.wear.watchface.client.OverlayStyle getOverlayStyle();
     method @kotlin.jvm.Throws(exceptionClasses=RemoteException::class) public java.time.Instant getPreviewReferenceInstant();
     method @kotlin.jvm.Throws(exceptionClasses=RemoteException::class) public androidx.wear.watchface.style.UserStyleSchema getUserStyleSchema();
+    method public default boolean isComplicationDisplayPolicySupported();
     method @AnyThread public boolean isConnectionAlive();
     method @kotlin.jvm.Throws(exceptionClasses=RemoteException::class) public void performAmbientTick() throws android.os.RemoteException;
     method @AnyThread public void removeClientDisconnectListener(androidx.wear.watchface.client.InteractiveWatchFaceClient.ClientDisconnectListener listener);
diff --git a/wear/watchface/watchface-client/api/public_plus_experimental_current.txt b/wear/watchface/watchface-client/api/public_plus_experimental_current.txt
index 5d787a5..f0ae340 100644
--- a/wear/watchface/watchface-client/api/public_plus_experimental_current.txt
+++ b/wear/watchface/watchface-client/api/public_plus_experimental_current.txt
@@ -146,6 +146,7 @@
     method @kotlin.jvm.Throws(exceptionClasses=RemoteException::class) public default androidx.wear.watchface.client.OverlayStyle getOverlayStyle();
     method @kotlin.jvm.Throws(exceptionClasses=RemoteException::class) public java.time.Instant getPreviewReferenceInstant();
     method @kotlin.jvm.Throws(exceptionClasses=RemoteException::class) public androidx.wear.watchface.style.UserStyleSchema getUserStyleSchema();
+    method public default boolean isComplicationDisplayPolicySupported();
     method @AnyThread public boolean isConnectionAlive();
     method @kotlin.jvm.Throws(exceptionClasses=RemoteException::class) public void performAmbientTick() throws android.os.RemoteException;
     method @AnyThread public void removeClientDisconnectListener(androidx.wear.watchface.client.InteractiveWatchFaceClient.ClientDisconnectListener listener);
diff --git a/wear/watchface/watchface-client/api/restricted_current.txt b/wear/watchface/watchface-client/api/restricted_current.txt
index 40bcc03..aba37f5 100644
--- a/wear/watchface/watchface-client/api/restricted_current.txt
+++ b/wear/watchface/watchface-client/api/restricted_current.txt
@@ -141,6 +141,7 @@
     method @kotlin.jvm.Throws(exceptionClasses=RemoteException::class) public default androidx.wear.watchface.client.OverlayStyle getOverlayStyle();
     method @kotlin.jvm.Throws(exceptionClasses=RemoteException::class) public java.time.Instant getPreviewReferenceInstant();
     method @kotlin.jvm.Throws(exceptionClasses=RemoteException::class) public androidx.wear.watchface.style.UserStyleSchema getUserStyleSchema();
+    method public default boolean isComplicationDisplayPolicySupported();
     method @AnyThread public boolean isConnectionAlive();
     method @kotlin.jvm.Throws(exceptionClasses=RemoteException::class) public void performAmbientTick() throws android.os.RemoteException;
     method @AnyThread public void removeClientDisconnectListener(androidx.wear.watchface.client.InteractiveWatchFaceClient.ClientDisconnectListener listener);
diff --git a/wear/watchface/watchface-client/src/androidTest/java/androidx/wear/watchface/client/test/WatchFaceControlClientTest.kt b/wear/watchface/watchface-client/src/androidTest/java/androidx/wear/watchface/client/test/WatchFaceControlClientTest.kt
index 1d381d8..ad4a176 100644
--- a/wear/watchface/watchface-client/src/androidTest/java/androidx/wear/watchface/client/test/WatchFaceControlClientTest.kt
+++ b/wear/watchface/watchface-client/src/androidTest/java/androidx/wear/watchface/client/test/WatchFaceControlClientTest.kt
@@ -28,6 +28,7 @@
 import android.graphics.SurfaceTexture
 import android.os.Build
 import android.os.Handler
+import android.os.IBinder
 import android.os.Looper
 import android.view.Surface
 import android.view.SurfaceHolder
@@ -53,6 +54,7 @@
 import androidx.wear.watchface.client.DisconnectReasons
 import androidx.wear.watchface.client.HeadlessWatchFaceClient
 import androidx.wear.watchface.client.InteractiveWatchFaceClient
+import androidx.wear.watchface.client.InteractiveWatchFaceClientImpl
 import androidx.wear.watchface.client.WatchFaceClientExperimental
 import androidx.wear.watchface.client.WatchFaceControlClient
 import androidx.wear.watchface.client.WatchUiState
@@ -69,6 +71,7 @@
 import androidx.wear.watchface.complications.data.RangedValueComplicationData
 import androidx.wear.watchface.complications.data.ShortTextComplicationData
 import androidx.wear.watchface.complications.data.toApiComplicationData
+import androidx.wear.watchface.control.IInteractiveWatchFace
 import androidx.wear.watchface.control.WatchFaceControlService
 import androidx.wear.watchface.samples.ExampleCanvasAnalogWatchFaceService
 import androidx.wear.watchface.samples.ExampleCanvasAnalogWatchFaceService.Companion.BLUE_STYLE
@@ -114,6 +117,8 @@
 import org.junit.runner.RunWith
 import org.mockito.Mock
 import org.mockito.Mockito
+import org.mockito.Mockito.mock
+import org.mockito.Mockito.`when`
 import org.mockito.MockitoAnnotations
 
 private const val CONNECT_TIMEOUT_MILLIS = 500L
@@ -944,6 +949,36 @@
     }
 
     @Test
+    public fun isComplicationDisplayPolicySupported() {
+        val wallpaperService = TestWatchfaceOverlayStyleWatchFaceService(
+            context,
+            surfaceHolder,
+            WatchFace.OverlayStyle(Color.valueOf(Color.RED), Color.valueOf(Color.BLACK))
+        )
+        val interactiveInstance = getOrCreateTestSubject(wallpaperService)
+
+        assertThat(interactiveInstance.isComplicationDisplayPolicySupported()).isTrue()
+
+        interactiveInstance.close()
+    }
+
+    @Test
+    public fun isComplicationDisplayPolicySupported_oldApi() {
+        val mockIInteractiveWatchFace = mock(IInteractiveWatchFace::class.java)
+        val mockIBinder = mock(IBinder::class.java)
+        `when`(mockIInteractiveWatchFace.asBinder()).thenReturn(mockIBinder)
+        `when`(mockIInteractiveWatchFace.apiVersion).thenReturn(6)
+
+        val interactiveInstance = InteractiveWatchFaceClientImpl(
+            mockIInteractiveWatchFace,
+            previewImageUpdateRequestedExecutor = null,
+            previewImageUpdateRequestedListener = null
+        )
+
+        assertThat(interactiveInstance.isComplicationDisplayPolicySupported()).isFalse()
+    }
+
+    @Test
     fun watchfaceOverlayStyle() {
         val wallpaperService = TestWatchfaceOverlayStyleWatchFaceService(
             context,
diff --git a/wear/watchface/watchface-client/src/main/java/androidx/wear/watchface/client/InteractiveWatchFaceClient.kt b/wear/watchface/watchface-client/src/main/java/androidx/wear/watchface/client/InteractiveWatchFaceClient.kt
index ca3b8d3..21d7a66 100644
--- a/wear/watchface/watchface-client/src/main/java/androidx/wear/watchface/client/InteractiveWatchFaceClient.kt
+++ b/wear/watchface/watchface-client/src/main/java/androidx/wear/watchface/client/InteractiveWatchFaceClient.kt
@@ -26,6 +26,7 @@
 import androidx.annotation.Px
 import androidx.annotation.RequiresApi
 import androidx.wear.watchface.complications.data.ComplicationData
+import androidx.wear.watchface.complications.data.ComplicationDisplayPolicy
 import androidx.wear.watchface.complications.data.toApiComplicationText
 import androidx.wear.watchface.utility.TraceEvent
 import androidx.wear.watchface.ComplicationSlot
@@ -338,6 +339,14 @@
     @OptIn(WatchFaceExperimental::class)
     @WatchFaceClientExperimental
     public fun removeOnWatchFaceColorsListener(listener: Consumer<WatchFaceColors?>) {}
+
+    /**
+     * Whether or not the watch face supports [ComplicationDisplayPolicy]. If it doesn't then the
+     * client is responsible for emulating it by observing the state of the keyguard and sending
+     * NoData complications when the device becomes locked and subsequently restoring them when it
+     * becomes unlocked for affected complications.
+     */
+    public fun isComplicationDisplayPolicySupported() = false
 }
 
 /** Controls a stateful remote interactive watch face. */
@@ -692,4 +701,6 @@
             }?.key
         }
     }
+
+    override fun isComplicationDisplayPolicySupported() = iInteractiveWatchFace.apiVersion >= 7
 }
diff --git a/wear/watchface/watchface-complications-data/src/main/java/androidx/wear/watchface/complications/data/ComplicationDataExpressionEvaluator.kt b/wear/watchface/watchface-complications-data/src/main/java/androidx/wear/watchface/complications/data/ComplicationDataExpressionEvaluator.kt
index e7de294..66f4757 100644
--- a/wear/watchface/watchface-complications-data/src/main/java/androidx/wear/watchface/complications/data/ComplicationDataExpressionEvaluator.kt
+++ b/wear/watchface/watchface-complications-data/src/main/java/androidx/wear/watchface/complications/data/ComplicationDataExpressionEvaluator.kt
@@ -19,6 +19,7 @@
 import androidx.annotation.GuardedBy
 import androidx.annotation.RestrictTo
 import java.util.concurrent.Executor
+import java.util.function.Consumer
 import kotlinx.coroutines.CoroutineScope
 import kotlinx.coroutines.asCoroutineDispatcher
 import kotlinx.coroutines.cancel
@@ -65,7 +66,7 @@
             listeners[listener] = CoroutineScope(executor.asCoroutineDispatcher()).apply {
                 launch {
                     data.collect {
-                        if (it != null) listener(it)
+                        if (it != null) listener.accept(it)
                     }
                 }
             }
@@ -90,4 +91,4 @@
     }
 }
 
-private typealias Listener = (WireComplicationData) -> Unit
+private typealias Listener = Consumer<WireComplicationData>
diff --git a/wear/watchface/watchface-complications-data/src/test/java/androidx/wear/watchface/complications/data/ComplicationDataExpressionEvaluatorTest.kt b/wear/watchface/watchface-complications-data/src/test/java/androidx/wear/watchface/complications/data/ComplicationDataExpressionEvaluatorTest.kt
index 0c18a65..5643563 100644
--- a/wear/watchface/watchface-complications-data/src/test/java/androidx/wear/watchface/complications/data/ComplicationDataExpressionEvaluatorTest.kt
+++ b/wear/watchface/watchface-complications-data/src/test/java/androidx/wear/watchface/complications/data/ComplicationDataExpressionEvaluatorTest.kt
@@ -24,6 +24,7 @@
 import com.nhaarman.mockitokotlin2.times
 import com.nhaarman.mockitokotlin2.verify
 import java.util.concurrent.Executor
+import java.util.function.Consumer
 import kotlin.coroutines.ContinuationInterceptor
 import kotlin.coroutines.CoroutineContext
 import kotlinx.coroutines.CoroutineDispatcher
@@ -39,7 +40,7 @@
 @RunWith(SharedRobolectricTestRunner::class)
 @OptIn(ExperimentalCoroutinesApi::class)
 class ComplicationDataExpressionEvaluatorTest {
-    private val listener = mock<(WireComplicationData) -> Unit>()
+    private val listener = mock<Consumer<WireComplicationData>>()
 
     @Before
     fun setup() {
@@ -73,7 +74,7 @@
         evaluator.addListener(coroutineContext.asExecutor(), listener)
         advanceUntilIdle()
 
-        verify(listener, never()).invoke(any())
+        verify(listener, never()).accept(any())
     }
 
     @Test
@@ -84,7 +85,7 @@
         evaluator.addListener(coroutineContext.asExecutor(), listener)
         advanceUntilIdle()
 
-        verify(listener, times(1)).invoke(UNEVALUATED_DATA)
+        verify(listener, times(1)).accept(UNEVALUATED_DATA)
     }
 
     @Test
@@ -96,7 +97,7 @@
         evaluator.init() // Should trigger a second call with UNEVALUATED_DATA.
         advanceUntilIdle()
 
-        verify(listener, never()).invoke(any())
+        verify(listener, never()).accept(any())
     }
 
     private companion object {
diff --git a/wear/watchface/watchface-style/src/androidTest/java/androidx/wear/watchface/style/UserStyleSettingWithStringResourcesTest.kt b/wear/watchface/watchface-style/src/androidTest/java/androidx/wear/watchface/style/UserStyleSettingWithStringResourcesTest.kt
index b88eadc..4fd0c50 100644
--- a/wear/watchface/watchface-style/src/androidTest/java/androidx/wear/watchface/style/UserStyleSettingWithStringResourcesTest.kt
+++ b/wear/watchface/watchface-style/src/androidTest/java/androidx/wear/watchface/style/UserStyleSettingWithStringResourcesTest.kt
@@ -241,6 +241,56 @@
     }
 
     @Test
+    public fun complicationSettingsWithIndices() {
+        val one = UserStyleSetting.Id("one")
+        val two = UserStyleSetting.Id("two")
+        val schema = UserStyleSchema(
+            listOf(
+                ListUserStyleSetting(
+                    one,
+                    context.resources,
+                    R.string.ith_style,
+                    R.string.ith_style_screen_reader_name,
+                    icon = null,
+                    options = listOf(
+                        ListOption(
+                            UserStyleSetting.Option.Id("one"),
+                            context.resources,
+                            R.string.ith_option,
+                            R.string.ith_option_screen_reader_name,
+                            icon = null
+                        )
+                    ),
+                    listOf(WatchFaceLayer.BASE, WatchFaceLayer.COMPLICATIONS_OVERLAY)
+                ),
+                ComplicationSlotsUserStyleSetting(
+                    two,
+                    context.resources,
+                    R.string.ith_style,
+                    R.string.ith_style_screen_reader_name,
+                    icon = null,
+                    complicationConfig = listOf(
+                        ComplicationSlotsOption(
+                            UserStyleSetting.Option.Id("one"),
+                            context.resources,
+                            R.string.ith_option,
+                            R.string.ith_option_screen_reader_name,
+                            icon = null,
+                            emptyList()
+                        )
+                    ),
+                    listOf(WatchFaceLayer.COMPLICATIONS)
+                )
+            )
+        )
+
+        Truth.assertThat(schema[one]!!.displayName).isEqualTo("1st style")
+        Truth.assertThat(schema[one]!!.description).isEqualTo("1st style setting")
+        Truth.assertThat(schema[two]!!.displayName).isEqualTo("2nd style")
+        Truth.assertThat(schema[two]!!.description).isEqualTo("2nd style setting")
+    }
+
+    @Test
     @Suppress("deprecation")
     public fun
     complicationsUserStyleSettingWireFormatRoundTrip_noScreenReaderName_filledByDisplayName() {
diff --git a/wear/watchface/watchface-style/src/androidTest/res/values/strings.xml b/wear/watchface/watchface-style/src/androidTest/res/values/strings.xml
index 3133a45..aa37205 100644
--- a/wear/watchface/watchface-style/src/androidTest/res/values/strings.xml
+++ b/wear/watchface/watchface-style/src/androidTest/res/values/strings.xml
@@ -36,6 +36,9 @@
     <string name="ith_option" translatable="false">%1$s option</string>
     <string name="ith_option_screen_reader_name" translatable="false">%1$s list option</string>
 
+    <string name="ith_style" translatable="false">%1$s style</string>
+    <string name="ith_style_screen_reader_name" translatable="false">%1$s style setting</string>
+
     <!-- An option within the watch face color style theme settings [CHAR LIMIT=20] -->
     <string name="colors_style_red">Red</string>
 
diff --git a/wear/watchface/watchface-style/src/main/java/androidx/wear/watchface/style/CurrentUserStyleRepository.kt b/wear/watchface/watchface-style/src/main/java/androidx/wear/watchface/style/CurrentUserStyleRepository.kt
index d5c8fff..fe8e80f 100644
--- a/wear/watchface/watchface-style/src/main/java/androidx/wear/watchface/style/CurrentUserStyleRepository.kt
+++ b/wear/watchface/watchface-style/src/main/java/androidx/wear/watchface/style/CurrentUserStyleRepository.kt
@@ -432,7 +432,6 @@
 public class UserStyleSchema constructor(
     userStyleSettings: List<UserStyleSetting>
 ) {
-
     public val userStyleSettings = userStyleSettings
         @Deprecated("use rootUserStyleSettings instead")
         get
@@ -503,7 +502,11 @@
     init {
         var complicationSlotsUserStyleSettingCount = 0
         var customValueUserStyleSettingCount = 0
+        var displayNameIndex = 1
         for (setting in userStyleSettings) {
+            // Provide the ordinal used by fallback descriptions for each setting.
+            setting.setDisplayNameIndex(displayNameIndex++)
+
             when (setting) {
                 is UserStyleSetting.ComplicationSlotsUserStyleSetting ->
                     complicationSlotsUserStyleSettingCount++
diff --git a/wear/watchface/watchface-style/src/main/java/androidx/wear/watchface/style/UserStyleSetting.kt b/wear/watchface/watchface-style/src/main/java/androidx/wear/watchface/style/UserStyleSetting.kt
index 483ff43..097a0b2e 100644
--- a/wear/watchface/watchface-style/src/main/java/androidx/wear/watchface/style/UserStyleSetting.kt
+++ b/wear/watchface/watchface-style/src/main/java/androidx/wear/watchface/style/UserStyleSetting.kt
@@ -177,6 +177,16 @@
         }
     }
 
+    internal fun setDisplayNameIndex(index: Int) {
+        if (displayNameInternal is DisplayText.ResourceDisplayTextWithIndex) {
+            displayNameInternal.setIndex(index)
+        }
+
+        if (descriptionInternal is DisplayText.ResourceDisplayTextWithIndex) {
+            descriptionInternal.setIndex(index)
+        }
+    }
+
     /**
      * Optional data for an on watch face editor (not the companion editor).
      *
@@ -788,8 +798,8 @@
             watchFaceEditorData: WatchFaceEditorData? = null
         ) : super(
             id,
-            DisplayText.ResourceDisplayText(resources, displayNameResourceId),
-            DisplayText.ResourceDisplayText(resources, descriptionResourceId),
+            DisplayText.ResourceDisplayTextWithIndex(resources, displayNameResourceId),
+            DisplayText.ResourceDisplayTextWithIndex(resources, descriptionResourceId),
             icon,
             watchFaceEditorData,
             listOf(BooleanOption.TRUE, BooleanOption.FALSE),
@@ -1351,8 +1361,8 @@
             watchFaceEditorData: WatchFaceEditorData? = null
         ) : this(
             id,
-            DisplayText.ResourceDisplayText(resources, displayNameResourceId),
-            DisplayText.ResourceDisplayText(resources, descriptionResourceId),
+            DisplayText.ResourceDisplayTextWithIndex(resources, displayNameResourceId),
+            DisplayText.ResourceDisplayTextWithIndex(resources, descriptionResourceId),
             icon,
             watchFaceEditorData,
             complicationConfig,
@@ -1952,8 +1962,8 @@
             watchFaceEditorData: WatchFaceEditorData? = null
         ) : super(
             id,
-            DisplayText.ResourceDisplayText(resources, displayNameResourceId),
-            DisplayText.ResourceDisplayText(resources, descriptionResourceId),
+            DisplayText.ResourceDisplayTextWithIndex(resources, displayNameResourceId),
+            DisplayText.ResourceDisplayTextWithIndex(resources, descriptionResourceId),
             icon,
             watchFaceEditorData,
             createOptionsList(minimumValue, maximumValue, defaultValue),
@@ -2155,8 +2165,8 @@
             watchFaceEditorData: WatchFaceEditorData? = null
         ) : super(
             id,
-            DisplayText.ResourceDisplayText(resources, displayNameResourceId),
-            DisplayText.ResourceDisplayText(resources, descriptionResourceId),
+            DisplayText.ResourceDisplayTextWithIndex(resources, displayNameResourceId),
+            DisplayText.ResourceDisplayTextWithIndex(resources, descriptionResourceId),
             icon,
             watchFaceEditorData,
             options,
@@ -2739,8 +2749,8 @@
             watchFaceEditorData: WatchFaceEditorData? = null
         ) : super(
             id,
-            DisplayText.ResourceDisplayText(resources, displayNameResourceId),
-            DisplayText.ResourceDisplayText(resources, descriptionResourceId),
+            DisplayText.ResourceDisplayTextWithIndex(resources, displayNameResourceId),
+            DisplayText.ResourceDisplayTextWithIndex(resources, descriptionResourceId),
             icon,
             watchFaceEditorData,
             createOptionsList(minimumValue, maximumValue, defaultValue),
diff --git a/wear/watchface/watchface/api/current.txt b/wear/watchface/watchface/api/current.txt
index 1319949..c61b6eb 100644
--- a/wear/watchface/watchface/api/current.txt
+++ b/wear/watchface/watchface/api/current.txt
@@ -237,6 +237,7 @@
   }
 
   @Deprecated public abstract static class Renderer.GlesRenderer extends androidx.wear.watchface.Renderer {
+    ctor @Deprecated @WorkerThread @kotlin.jvm.Throws(exceptionClasses=GlesException::class) public Renderer.GlesRenderer(android.view.SurfaceHolder surfaceHolder, androidx.wear.watchface.style.CurrentUserStyleRepository currentUserStyleRepository, androidx.wear.watchface.WatchState watchState, @IntRange(from=0L, to=60000L) long interactiveDrawModeUpdateDelayMillis, optional int[] eglConfigAttribList, optional int[] eglSurfaceAttribList, optional int[] eglContextAttribList) throws androidx.wear.watchface.Renderer.GlesRenderer.GlesException;
     ctor @Deprecated @WorkerThread @kotlin.jvm.Throws(exceptionClasses=GlesException::class) public Renderer.GlesRenderer(android.view.SurfaceHolder surfaceHolder, androidx.wear.watchface.style.CurrentUserStyleRepository currentUserStyleRepository, androidx.wear.watchface.WatchState watchState, @IntRange(from=0L, to=60000L) long interactiveDrawModeUpdateDelayMillis, optional int[] eglConfigAttribList, optional int[] eglSurfaceAttribList) throws androidx.wear.watchface.Renderer.GlesRenderer.GlesException;
     ctor @Deprecated @WorkerThread @kotlin.jvm.Throws(exceptionClasses=GlesException::class) public Renderer.GlesRenderer(android.view.SurfaceHolder surfaceHolder, androidx.wear.watchface.style.CurrentUserStyleRepository currentUserStyleRepository, androidx.wear.watchface.WatchState watchState, @IntRange(from=0L, to=60000L) long interactiveDrawModeUpdateDelayMillis, optional int[] eglConfigAttribList) throws androidx.wear.watchface.Renderer.GlesRenderer.GlesException;
     ctor @Deprecated @WorkerThread @kotlin.jvm.Throws(exceptionClasses=GlesException::class) public Renderer.GlesRenderer(android.view.SurfaceHolder surfaceHolder, androidx.wear.watchface.style.CurrentUserStyleRepository currentUserStyleRepository, androidx.wear.watchface.WatchState watchState, @IntRange(from=0L, to=60000L) long interactiveDrawModeUpdateDelayMillis) throws androidx.wear.watchface.Renderer.GlesRenderer.GlesException;
@@ -264,6 +265,7 @@
   }
 
   public abstract static class Renderer.GlesRenderer2<SharedAssetsT extends androidx.wear.watchface.Renderer.SharedAssets> extends androidx.wear.watchface.Renderer.GlesRenderer {
+    ctor @WorkerThread @kotlin.jvm.Throws(exceptionClasses=GlesException::class) public Renderer.GlesRenderer2(android.view.SurfaceHolder surfaceHolder, androidx.wear.watchface.style.CurrentUserStyleRepository currentUserStyleRepository, androidx.wear.watchface.WatchState watchState, @IntRange(from=0L, to=60000L) long interactiveDrawModeUpdateDelayMillis, optional int[] eglConfigAttribList, optional int[] eglSurfaceAttribList, optional int[] eglContextAttribList) throws androidx.wear.watchface.Renderer.GlesRenderer.GlesException;
     ctor @WorkerThread @kotlin.jvm.Throws(exceptionClasses=GlesException::class) public Renderer.GlesRenderer2(android.view.SurfaceHolder surfaceHolder, androidx.wear.watchface.style.CurrentUserStyleRepository currentUserStyleRepository, androidx.wear.watchface.WatchState watchState, @IntRange(from=0L, to=60000L) long interactiveDrawModeUpdateDelayMillis, optional int[] eglConfigAttribList, optional int[] eglSurfaceAttribList) throws androidx.wear.watchface.Renderer.GlesRenderer.GlesException;
     ctor @WorkerThread @kotlin.jvm.Throws(exceptionClasses=GlesException::class) public Renderer.GlesRenderer2(android.view.SurfaceHolder surfaceHolder, androidx.wear.watchface.style.CurrentUserStyleRepository currentUserStyleRepository, androidx.wear.watchface.WatchState watchState, @IntRange(from=0L, to=60000L) long interactiveDrawModeUpdateDelayMillis, optional int[] eglConfigAttribList) throws androidx.wear.watchface.Renderer.GlesRenderer.GlesException;
     ctor @WorkerThread @kotlin.jvm.Throws(exceptionClasses=GlesException::class) public Renderer.GlesRenderer2(android.view.SurfaceHolder surfaceHolder, androidx.wear.watchface.style.CurrentUserStyleRepository currentUserStyleRepository, androidx.wear.watchface.WatchState watchState, @IntRange(from=0L, to=60000L) long interactiveDrawModeUpdateDelayMillis) throws androidx.wear.watchface.Renderer.GlesRenderer.GlesException;
diff --git a/wear/watchface/watchface/api/public_plus_experimental_current.txt b/wear/watchface/watchface/api/public_plus_experimental_current.txt
index 8965e97..99d275b 100644
--- a/wear/watchface/watchface/api/public_plus_experimental_current.txt
+++ b/wear/watchface/watchface/api/public_plus_experimental_current.txt
@@ -256,6 +256,7 @@
   }
 
   @Deprecated public abstract static class Renderer.GlesRenderer extends androidx.wear.watchface.Renderer {
+    ctor @Deprecated @WorkerThread @kotlin.jvm.Throws(exceptionClasses=GlesException::class) public Renderer.GlesRenderer(android.view.SurfaceHolder surfaceHolder, androidx.wear.watchface.style.CurrentUserStyleRepository currentUserStyleRepository, androidx.wear.watchface.WatchState watchState, @IntRange(from=0L, to=60000L) long interactiveDrawModeUpdateDelayMillis, optional int[] eglConfigAttribList, optional int[] eglSurfaceAttribList, optional int[] eglContextAttribList) throws androidx.wear.watchface.Renderer.GlesRenderer.GlesException;
     ctor @Deprecated @WorkerThread @kotlin.jvm.Throws(exceptionClasses=GlesException::class) public Renderer.GlesRenderer(android.view.SurfaceHolder surfaceHolder, androidx.wear.watchface.style.CurrentUserStyleRepository currentUserStyleRepository, androidx.wear.watchface.WatchState watchState, @IntRange(from=0L, to=60000L) long interactiveDrawModeUpdateDelayMillis, optional int[] eglConfigAttribList, optional int[] eglSurfaceAttribList) throws androidx.wear.watchface.Renderer.GlesRenderer.GlesException;
     ctor @Deprecated @WorkerThread @kotlin.jvm.Throws(exceptionClasses=GlesException::class) public Renderer.GlesRenderer(android.view.SurfaceHolder surfaceHolder, androidx.wear.watchface.style.CurrentUserStyleRepository currentUserStyleRepository, androidx.wear.watchface.WatchState watchState, @IntRange(from=0L, to=60000L) long interactiveDrawModeUpdateDelayMillis, optional int[] eglConfigAttribList) throws androidx.wear.watchface.Renderer.GlesRenderer.GlesException;
     ctor @Deprecated @WorkerThread @kotlin.jvm.Throws(exceptionClasses=GlesException::class) public Renderer.GlesRenderer(android.view.SurfaceHolder surfaceHolder, androidx.wear.watchface.style.CurrentUserStyleRepository currentUserStyleRepository, androidx.wear.watchface.WatchState watchState, @IntRange(from=0L, to=60000L) long interactiveDrawModeUpdateDelayMillis) throws androidx.wear.watchface.Renderer.GlesRenderer.GlesException;
@@ -283,6 +284,7 @@
   }
 
   public abstract static class Renderer.GlesRenderer2<SharedAssetsT extends androidx.wear.watchface.Renderer.SharedAssets> extends androidx.wear.watchface.Renderer.GlesRenderer {
+    ctor @WorkerThread @kotlin.jvm.Throws(exceptionClasses=GlesException::class) public Renderer.GlesRenderer2(android.view.SurfaceHolder surfaceHolder, androidx.wear.watchface.style.CurrentUserStyleRepository currentUserStyleRepository, androidx.wear.watchface.WatchState watchState, @IntRange(from=0L, to=60000L) long interactiveDrawModeUpdateDelayMillis, optional int[] eglConfigAttribList, optional int[] eglSurfaceAttribList, optional int[] eglContextAttribList) throws androidx.wear.watchface.Renderer.GlesRenderer.GlesException;
     ctor @WorkerThread @kotlin.jvm.Throws(exceptionClasses=GlesException::class) public Renderer.GlesRenderer2(android.view.SurfaceHolder surfaceHolder, androidx.wear.watchface.style.CurrentUserStyleRepository currentUserStyleRepository, androidx.wear.watchface.WatchState watchState, @IntRange(from=0L, to=60000L) long interactiveDrawModeUpdateDelayMillis, optional int[] eglConfigAttribList, optional int[] eglSurfaceAttribList) throws androidx.wear.watchface.Renderer.GlesRenderer.GlesException;
     ctor @WorkerThread @kotlin.jvm.Throws(exceptionClasses=GlesException::class) public Renderer.GlesRenderer2(android.view.SurfaceHolder surfaceHolder, androidx.wear.watchface.style.CurrentUserStyleRepository currentUserStyleRepository, androidx.wear.watchface.WatchState watchState, @IntRange(from=0L, to=60000L) long interactiveDrawModeUpdateDelayMillis, optional int[] eglConfigAttribList) throws androidx.wear.watchface.Renderer.GlesRenderer.GlesException;
     ctor @WorkerThread @kotlin.jvm.Throws(exceptionClasses=GlesException::class) public Renderer.GlesRenderer2(android.view.SurfaceHolder surfaceHolder, androidx.wear.watchface.style.CurrentUserStyleRepository currentUserStyleRepository, androidx.wear.watchface.WatchState watchState, @IntRange(from=0L, to=60000L) long interactiveDrawModeUpdateDelayMillis) throws androidx.wear.watchface.Renderer.GlesRenderer.GlesException;
diff --git a/wear/watchface/watchface/api/restricted_current.txt b/wear/watchface/watchface/api/restricted_current.txt
index 1319949..c61b6eb 100644
--- a/wear/watchface/watchface/api/restricted_current.txt
+++ b/wear/watchface/watchface/api/restricted_current.txt
@@ -237,6 +237,7 @@
   }
 
   @Deprecated public abstract static class Renderer.GlesRenderer extends androidx.wear.watchface.Renderer {
+    ctor @Deprecated @WorkerThread @kotlin.jvm.Throws(exceptionClasses=GlesException::class) public Renderer.GlesRenderer(android.view.SurfaceHolder surfaceHolder, androidx.wear.watchface.style.CurrentUserStyleRepository currentUserStyleRepository, androidx.wear.watchface.WatchState watchState, @IntRange(from=0L, to=60000L) long interactiveDrawModeUpdateDelayMillis, optional int[] eglConfigAttribList, optional int[] eglSurfaceAttribList, optional int[] eglContextAttribList) throws androidx.wear.watchface.Renderer.GlesRenderer.GlesException;
     ctor @Deprecated @WorkerThread @kotlin.jvm.Throws(exceptionClasses=GlesException::class) public Renderer.GlesRenderer(android.view.SurfaceHolder surfaceHolder, androidx.wear.watchface.style.CurrentUserStyleRepository currentUserStyleRepository, androidx.wear.watchface.WatchState watchState, @IntRange(from=0L, to=60000L) long interactiveDrawModeUpdateDelayMillis, optional int[] eglConfigAttribList, optional int[] eglSurfaceAttribList) throws androidx.wear.watchface.Renderer.GlesRenderer.GlesException;
     ctor @Deprecated @WorkerThread @kotlin.jvm.Throws(exceptionClasses=GlesException::class) public Renderer.GlesRenderer(android.view.SurfaceHolder surfaceHolder, androidx.wear.watchface.style.CurrentUserStyleRepository currentUserStyleRepository, androidx.wear.watchface.WatchState watchState, @IntRange(from=0L, to=60000L) long interactiveDrawModeUpdateDelayMillis, optional int[] eglConfigAttribList) throws androidx.wear.watchface.Renderer.GlesRenderer.GlesException;
     ctor @Deprecated @WorkerThread @kotlin.jvm.Throws(exceptionClasses=GlesException::class) public Renderer.GlesRenderer(android.view.SurfaceHolder surfaceHolder, androidx.wear.watchface.style.CurrentUserStyleRepository currentUserStyleRepository, androidx.wear.watchface.WatchState watchState, @IntRange(from=0L, to=60000L) long interactiveDrawModeUpdateDelayMillis) throws androidx.wear.watchface.Renderer.GlesRenderer.GlesException;
@@ -264,6 +265,7 @@
   }
 
   public abstract static class Renderer.GlesRenderer2<SharedAssetsT extends androidx.wear.watchface.Renderer.SharedAssets> extends androidx.wear.watchface.Renderer.GlesRenderer {
+    ctor @WorkerThread @kotlin.jvm.Throws(exceptionClasses=GlesException::class) public Renderer.GlesRenderer2(android.view.SurfaceHolder surfaceHolder, androidx.wear.watchface.style.CurrentUserStyleRepository currentUserStyleRepository, androidx.wear.watchface.WatchState watchState, @IntRange(from=0L, to=60000L) long interactiveDrawModeUpdateDelayMillis, optional int[] eglConfigAttribList, optional int[] eglSurfaceAttribList, optional int[] eglContextAttribList) throws androidx.wear.watchface.Renderer.GlesRenderer.GlesException;
     ctor @WorkerThread @kotlin.jvm.Throws(exceptionClasses=GlesException::class) public Renderer.GlesRenderer2(android.view.SurfaceHolder surfaceHolder, androidx.wear.watchface.style.CurrentUserStyleRepository currentUserStyleRepository, androidx.wear.watchface.WatchState watchState, @IntRange(from=0L, to=60000L) long interactiveDrawModeUpdateDelayMillis, optional int[] eglConfigAttribList, optional int[] eglSurfaceAttribList) throws androidx.wear.watchface.Renderer.GlesRenderer.GlesException;
     ctor @WorkerThread @kotlin.jvm.Throws(exceptionClasses=GlesException::class) public Renderer.GlesRenderer2(android.view.SurfaceHolder surfaceHolder, androidx.wear.watchface.style.CurrentUserStyleRepository currentUserStyleRepository, androidx.wear.watchface.WatchState watchState, @IntRange(from=0L, to=60000L) long interactiveDrawModeUpdateDelayMillis, optional int[] eglConfigAttribList) throws androidx.wear.watchface.Renderer.GlesRenderer.GlesException;
     ctor @WorkerThread @kotlin.jvm.Throws(exceptionClasses=GlesException::class) public Renderer.GlesRenderer2(android.view.SurfaceHolder surfaceHolder, androidx.wear.watchface.style.CurrentUserStyleRepository currentUserStyleRepository, androidx.wear.watchface.WatchState watchState, @IntRange(from=0L, to=60000L) long interactiveDrawModeUpdateDelayMillis) throws androidx.wear.watchface.Renderer.GlesRenderer.GlesException;
diff --git a/wear/watchface/watchface/src/main/java/androidx/wear/watchface/ComplicationSlot.kt b/wear/watchface/watchface/src/main/java/androidx/wear/watchface/ComplicationSlot.kt
index 565f2c9..88fa276 100644
--- a/wear/watchface/watchface/src/main/java/androidx/wear/watchface/ComplicationSlot.kt
+++ b/wear/watchface/watchface/src/main/java/androidx/wear/watchface/ComplicationSlot.kt
@@ -32,7 +32,6 @@
 import androidx.wear.watchface.complications.ComplicationSlotBounds
 import androidx.wear.watchface.complications.DefaultComplicationDataSourcePolicy
 import androidx.wear.watchface.complications.data.ComplicationData
-import androidx.wear.watchface.complications.data.ComplicationDataExpressionEvaluator
 import androidx.wear.watchface.complications.data.ComplicationDisplayPolicies
 import androidx.wear.watchface.complications.data.ComplicationExperimental
 import androidx.wear.watchface.complications.data.ComplicationType
@@ -44,12 +43,10 @@
 import androidx.wear.watchface.style.UserStyleSetting.ComplicationSlotsUserStyleSetting
 import androidx.wear.watchface.style.UserStyleSetting.ComplicationSlotsUserStyleSetting.ComplicationSlotOverlay
 import java.lang.Integer.min
-import java.time.Duration
 import java.time.Instant
 import java.time.LocalDateTime
 import java.time.ZoneId
 import java.time.ZonedDateTime
-import java.time.temporal.ChronoUnit.SECONDS
 import java.util.Objects
 import kotlin.math.abs
 import kotlin.math.atan2
@@ -57,7 +54,6 @@
 import kotlin.math.sin
 import kotlinx.coroutines.flow.MutableStateFlow
 import kotlinx.coroutines.flow.StateFlow
-import kotlinx.coroutines.launch
 
 /**
  * Interface for rendering complicationSlots onto a [Canvas]. These should be created by
@@ -429,8 +425,6 @@
         )
     }
 
-    private val wearSdkVersion by lazy { complicationSlotsManager.watchFaceHostApi.wearSdkVersion }
-
     private var lastComplicationUpdate = Instant.EPOCH
 
     private class ComplicationDataHistoryEntry(
@@ -973,16 +967,12 @@
 
     internal var dataDirty = true
 
-    private var lastExpressionEvaluator: ComplicationDataExpressionEvaluator? = null
-
-    private var unevaluatedComplicationData: ComplicationData = NoDataComplicationData()
-
     /**
      * The [androidx.wear.watchface.complications.data.ComplicationData] associated with the
      * [ComplicationSlot]. This defaults to [NoDataComplicationData].
      */
     public val complicationData: StateFlow<ComplicationData> =
-        MutableStateFlow(unevaluatedComplicationData)
+        MutableStateFlow(NoDataComplicationData())
 
     /**
      * The complication data sent by the system. This may contain a timeline out of which
@@ -1044,78 +1034,9 @@
             best = screenLockedFallback // This is NoDataComplicationData.
         }
 
-        if (wearSdkVersion >= Build.VERSION_CODES.TIRAMISU) {
-            best.evaluateAndLoadUpdates(loadDrawablesAsynchronous)
-            // Loading synchronously if forced.
-            if (forceUpdate) best.load(loadDrawablesAsynchronous)
-        } else {
-            // Avoid expression evaluation pre-T as it may be redacted by the old platform.
-            if (forceUpdate || complicationData.value != best) best.load(loadDrawablesAsynchronous)
-        }
-    }
-
-    private fun ComplicationData.load(loadDrawablesAsynchronous: Boolean) {
-        renderer.loadData(this, loadDrawablesAsynchronous)
-        (complicationData as MutableStateFlow).value = this
-    }
-
-    /**
-     * Creates a [ComplicationDataExpressionEvaluator] and monitors for updates, sending them to the
-     * [renderer].
-     *
-     * Ignores new data that has equivalent expression (see [ComplicationData.equalsUnevaluated]).
-     * While the data is first being evaluated, sends [NoDataComplicationData] to the renderer.
-     */
-    private fun ComplicationData.evaluateAndLoadUpdates(
-        loadDrawablesAsynchronous: Boolean,
-    ) {
-        if (unevaluatedComplicationData equalsUnevaluated this) return
-        unevaluatedComplicationData = this
-        // Reverting to NoData while evaluating.
-        NoDataComplicationData().load(loadDrawablesAsynchronous)
-        lastExpressionEvaluator?.close()
-        // TODO(b/260065006): Do we need to close the evaluator on destroy?
-        lastExpressionEvaluator =
-            ComplicationDataExpressionEvaluator(asWireComplicationData())
-                .apply {
-                    init()
-                    loadUpdates(loadDrawablesAsynchronous)
-                }
-    }
-
-    /**
-     * Monitors evaluated expression updates and sends them to the [renderer].
-     *
-     * If this is the first evaluation, loads the data immediately. Otherwise, triggers watchface
-     * invalidation on the next top of the second.
-     */
-    private fun ComplicationDataExpressionEvaluator.loadUpdates(
-        loadDrawablesAsynchronous: Boolean
-    ) {
-        complicationSlotsManager.watchFaceHostApi.getUiThreadCoroutineScope().launch {
-            data.collect { evaluatedWireData ->
-                if (evaluatedWireData == null) return@collect // Not yet evaluated.
-                val evaluatedData = evaluatedWireData.toApiComplicationData()
-                if (complicationData.value is NoDataComplicationData) {
-                    // Loading now if it's the first update.
-                    evaluatedData.load(loadDrawablesAsynchronous)
-                } else {
-                    // Loading in the next frame on further updates.
-                    (complicationData as MutableStateFlow).value = evaluatedData
-                    complicationSlotsManager.watchFaceHostApi.postInvalidate(
-                        durationUntilNextForcedFrame()
-                    )
-                }
-            }
-        }
-    }
-
-    /** Returns the duration until the next top of the second. */
-    private fun durationUntilNextForcedFrame(): Duration {
-        val now = Instant.ofEpochMilli(
-            complicationSlotsManager.watchFaceHostApi.systemTimeProvider.getSystemTimeMillis()
-        )
-        return Duration.between(now, (now + Duration.ofSeconds(1)).truncatedTo(SECONDS))
+        if (!forceUpdate && complicationData.value == best) return
+        renderer.loadData(best, loadDrawablesAsynchronous)
+        (complicationData as MutableStateFlow).value = best
     }
 
     /**
@@ -1210,8 +1131,7 @@
 
         if (isHeadless) {
             timelineComplicationData = EmptyComplicationData()
-            unevaluatedComplicationData = EmptyComplicationData()
-            (complicationData as MutableStateFlow).value = unevaluatedComplicationData
+            (complicationData as MutableStateFlow).value = EmptyComplicationData()
         }
     }
 
diff --git a/wear/watchface/watchface/src/main/java/androidx/wear/watchface/Renderer.kt b/wear/watchface/watchface/src/main/java/androidx/wear/watchface/Renderer.kt
index 3c3c77e..878108f 100644
--- a/wear/watchface/watchface/src/main/java/androidx/wear/watchface/Renderer.kt
+++ b/wear/watchface/watchface/src/main/java/androidx/wear/watchface/Renderer.kt
@@ -954,6 +954,8 @@
      * RGBA8888 back buffer.
      * @param eglSurfaceAttribList The attributes to be passed to [EGL14.eglCreateWindowSurface]. By
      * default this is empty.
+     * @param eglContextAttribList The attributes to be passed to [EGL14.eglCreateContext]. By
+     * default this selects [EGL14.EGL_CONTEXT_CLIENT_VERSION] 2.
      * @throws [GlesException] If any GL calls fail during initialization.
      */
     @Deprecated(message = "GlesRenderer is deprecated", ReplaceWith("GlesRenderer2"))
@@ -968,7 +970,8 @@
         @IntRange(from = 0, to = 60000)
         interactiveDrawModeUpdateDelayMillis: Long,
         private val eglConfigAttribList: IntArray = EGL_CONFIG_ATTRIB_LIST,
-        private val eglSurfaceAttribList: IntArray = EGL_SURFACE_ATTRIB_LIST
+        private val eglSurfaceAttribList: IntArray = EGL_SURFACE_ATTRIB_LIST,
+        private val eglContextAttribList: IntArray = EGL_CONTEXT_ATTRIB_LIST
     ) : Renderer(
         surfaceHolder,
         currentUserStyleRepository,
@@ -1213,7 +1216,7 @@
                         eglDisplay,
                         eglConfig,
                         EGL14.EGL_NO_CONTEXT,
-                        EGL_CONTEXT_ATTRIB_LIST,
+                        eglContextAttribList,
                         0
                     )
                     if (sharedAssetsHolder.eglBackgroundThreadContext == EGL14.EGL_NO_CONTEXT) {
@@ -1566,6 +1569,8 @@
      * RGBA8888 back buffer.
      * @param eglSurfaceAttribList The attributes to be passed to [EGL14.eglCreateWindowSurface]. By
      * default this is empty.
+     * @param eglContextAttribList The attributes to be passed to [EGL14.eglCreateContext]. By
+     * default this selects [EGL14.EGL_CONTEXT_CLIENT_VERSION] 2.
      * @throws [Renderer.GlesException] If any GL calls fail during initialization.
      */
     public abstract class GlesRenderer2<SharedAssetsT>
@@ -1579,14 +1584,16 @@
         @IntRange(from = 0, to = 60000)
         interactiveDrawModeUpdateDelayMillis: Long,
         eglConfigAttribList: IntArray = EGL_CONFIG_ATTRIB_LIST,
-        eglSurfaceAttribList: IntArray = EGL_SURFACE_ATTRIB_LIST
+        eglSurfaceAttribList: IntArray = EGL_SURFACE_ATTRIB_LIST,
+        eglContextAttribList: IntArray = EGL_CONTEXT_ATTRIB_LIST
     ) : GlesRenderer(
         surfaceHolder,
         currentUserStyleRepository,
         watchState,
         interactiveDrawModeUpdateDelayMillis,
         eglConfigAttribList,
-        eglSurfaceAttribList
+        eglSurfaceAttribList,
+        eglContextAttribList
     ) where SharedAssetsT : SharedAssets {
         /**
          * When editing multiple [WatchFaceService] instances and hence Renderers can exist
diff --git a/wear/watchface/watchface/src/test/java/androidx/wear/watchface/WatchFaceServiceTest.kt b/wear/watchface/watchface/src/test/java/androidx/wear/watchface/WatchFaceServiceTest.kt
index 48c3f56..b12863b 100644
--- a/wear/watchface/watchface/src/test/java/androidx/wear/watchface/WatchFaceServiceTest.kt
+++ b/wear/watchface/watchface/src/test/java/androidx/wear/watchface/WatchFaceServiceTest.kt
@@ -53,7 +53,6 @@
 import androidx.wear.watchface.complications.ComplicationSlotBounds
 import androidx.wear.watchface.complications.DefaultComplicationDataSourcePolicy
 import androidx.wear.watchface.complications.SystemDataSources
-import androidx.wear.watchface.complications.data.ComplicationData
 import androidx.wear.watchface.complications.data.ComplicationDisplayPolicies
 import androidx.wear.watchface.complications.data.ComplicationExperimental
 import androidx.wear.watchface.complications.data.ComplicationPersistencePolicies
@@ -65,7 +64,6 @@
 import androidx.wear.watchface.complications.data.NoDataComplicationData
 import androidx.wear.watchface.complications.data.PlainComplicationText
 import androidx.wear.watchface.complications.data.ShortTextComplicationData
-import androidx.wear.watchface.complications.data.StringExpression
 import androidx.wear.watchface.complications.data.TimeDifferenceComplicationText
 import androidx.wear.watchface.complications.data.TimeDifferenceStyle
 import androidx.wear.watchface.complications.rendering.CanvasComplicationDrawable
@@ -109,9 +107,6 @@
 import kotlinx.coroutines.Deferred
 import kotlinx.coroutines.Dispatchers
 import kotlinx.coroutines.android.asCoroutineDispatcher
-import kotlinx.coroutines.flow.Flow
-import kotlinx.coroutines.flow.dropWhile
-import kotlinx.coroutines.flow.first
 import kotlinx.coroutines.launch
 import kotlinx.coroutines.runBlocking
 import kotlinx.coroutines.withTimeout
@@ -2926,55 +2921,6 @@
     }
 
     @Test
-    @Config(sdk = [Build.VERSION_CODES.TIRAMISU])
-    public fun complicationDataExpression_evaluatesExpression(): Unit =
-        runBlocking(Dispatchers.Main.immediate) {
-            initWallpaperInteractiveWatchFaceInstance(
-                complicationSlots = listOf(leftComplication),
-            )
-
-            interactiveWatchFaceInstance.updateComplicationData(
-                listOf(
-                    IdAndComplicationDataWireFormat(
-                        LEFT_COMPLICATION_ID,
-                        WireComplicationData.Builder(WireComplicationData.TYPE_SHORT_TEXT)
-                            .setShortText(WireComplicationText(StringExpression(byteArrayOf(1, 2))))
-                            .build()
-                    )
-                )
-            )
-
-            val data = leftComplication.complicationData.firstNonEmpty().asWireComplicationData()
-            assertThat(data.shortText)
-                // TODO(b/260065006): Verify that it is actually evaluated.
-                .isEqualTo(WireComplicationText(StringExpression(byteArrayOf(1, 2))))
-        }
-
-    @Test
-    @Config(sdk = [Build.VERSION_CODES.R])
-    public fun complicationDataExpression_preT_doesNotEvaluateExpression(): Unit =
-        runBlocking(Dispatchers.Main.immediate) {
-            initWallpaperInteractiveWatchFaceInstance(
-                complicationSlots = listOf(leftComplication),
-            )
-
-            interactiveWatchFaceInstance.updateComplicationData(
-                listOf(
-                    IdAndComplicationDataWireFormat(
-                        LEFT_COMPLICATION_ID,
-                        WireComplicationData.Builder(WireComplicationData.TYPE_SHORT_TEXT)
-                            .setShortText(WireComplicationText(StringExpression(byteArrayOf(1, 2))))
-                            .build()
-                    )
-                )
-            )
-
-            val data = leftComplication.complicationData.firstNonEmpty().asWireComplicationData()
-            assertThat(data.shortText)
-                .isEqualTo(WireComplicationText(StringExpression(byteArrayOf(1, 2))))
-        }
-
-    @Test
     @Config(sdk = [Build.VERSION_CODES.O_MR1])
     public fun complicationCache() {
         val complicationCache = HashMap<String, ByteArray>()
@@ -6646,9 +6592,6 @@
         ).build()
 
     private suspend fun <T> Deferred<T>.awaitWithTimeout(): T = withTimeout(1000) { await() }
-
-    private suspend fun Flow<ComplicationData>.firstNonEmpty(): ComplicationData =
-        withTimeout(1000) { dropWhile { it is NoDataComplicationData }.first() }
 }
 
 class TestNopCanvasWatchFaceService : WatchFaceService() {
diff --git a/work/work-inspection/src/main/java/androidx/work/inspection/WorkManagerInspector.kt b/work/work-inspection/src/main/java/androidx/work/inspection/WorkManagerInspector.kt
index 66a0dd2..f51e4cf 100644
--- a/work/work-inspection/src/main/java/androidx/work/inspection/WorkManagerInspector.kt
+++ b/work/work-inspection/src/main/java/androidx/work/inspection/WorkManagerInspector.kt
@@ -133,13 +133,13 @@
                 owner,
                 object : Observer<T> {
                     private var lastValue: T? = null
-                    override fun onChanged(t: T) {
-                        if (t == null) {
+                    override fun onChanged(value: T) {
+                        if (value == null) {
                             removeObserver(this)
                         } else {
                             executor.execute {
-                                listener(lastValue, t)
-                                lastValue = t
+                                listener(lastValue, value)
+                                lastValue = value
                             }
                         }
                     }
diff --git a/work/work-runtime/src/androidTest/java/androidx/work/impl/utils/LiveDataUtilsTest.java b/work/work-runtime/src/androidTest/java/androidx/work/impl/utils/LiveDataUtilsTest.java
index bd21bae..c186329 100644
--- a/work/work-runtime/src/androidTest/java/androidx/work/impl/utils/LiveDataUtilsTest.java
+++ b/work/work-runtime/src/androidTest/java/androidx/work/impl/utils/LiveDataUtilsTest.java
@@ -124,7 +124,7 @@
         int mTimesUpdated;
 
         @Override
-        public void onChanged(@Nullable T t) {
+        public void onChanged(@Nullable T value) {
             ++mTimesUpdated;
         }
     }